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)
1012 lines
69 KiB
Markdown
1012 lines
69 KiB
Markdown
# 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
|
||
nå `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` på
|
||
`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` nå
|
||
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` på
|
||
`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) — ADR-023 + ADR-026
|
||
- **Status:** ✅ HELT FERDIG 2026-07-19 — kaptein/deltaker-delen (ADR-023)
|
||
OG tilskuer-delen (ADR-026, se eget punkt lenger ned).
|
||
- 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.
|
||
- **2026-07-19, ✅ BYGGET (ADR-023):** Kaptein er nå en REELL autorisasjonsrolle,
|
||
ikke bare et merke. `app/team_authz.py` sin nye `user_is_team_captain`
|
||
(erstatter `user_may_act_for_team`) krever `is_captain=true` (eller
|
||
org-eier/admin) for å legge til/fjerne deltakere og låse et lag
|
||
(`matches.py`). Bevisst unntak — funnet ved å faktisk sjekke ekte
|
||
produksjonsdata FØR utrulling: har laget INGEN utpekt kaptein ennå, godtas
|
||
enhver rostret spiller i stedet (ellers ville «De Unge»-laget i den ekte
|
||
«De Gamle er Eldst»-turneringen vært låst ute umiddelbart — 0 av 2
|
||
roster-rader er i dag merket kaptein der).
|
||
- **2026-07-19, ✅ BYGGET (ADR-023):** «Kun én kaptein per lag» håndheves nå —
|
||
`PATCH`/`POST .../roster` (`tournaments.py`) fjerner automatisk
|
||
kapteinmerket fra andre rader på samme lag når en ny kaptein settes.
|
||
Nødvendig konsekvens av at kaptein nå er en autorisasjonsrolle. Ingen
|
||
eksisterende lag hadde flere kapteiner (sjekket mot ekte data), så ingen
|
||
opprydning av data var nødvendig.
|
||
- **2026-07-19, ✅ BYGGET (ADR-023):** Score-føring/-korrigering begrenset til
|
||
matchens FAKTISKE deltakere (`app/team_authz.py` sin nye
|
||
`user_is_match_participant`, brukt av `scoring.py`) — ikke lenger «noen på
|
||
laget», og uavhengig av kapteinmerket. Erstatter «Scoring-autorisasjon»-
|
||
punktet under, spørsmål (a) er dermed besvart.
|
||
- **Tilskuer — ✅ HELT FERDIG, LIVE 2026-07-19 (ADR-026),** rett etter
|
||
Kommunikasjon (ADR-025) som gjorde feed-synligheten (som denne skulle
|
||
defineres sammen med) klar. Ingen ny rolle/tabell — «tilskuer» er ganske
|
||
enkelt enhver som kan SE turneringen (`tournament.visibility`), nå også
|
||
for LEADERBOARD, MATCH-LISTE og FULLT SCOREKORT (hull-for-hull), ikke bare
|
||
info-siden/programtider som før (`GET /public/tournaments/{id}/
|
||
leaderboard`, `.../sessions/{id}/matches`, `.../matches/{id}/scorecard`,
|
||
alle i `registration.py`, gjenbruker eksisterende
|
||
`fetch_leaderboard`/`fetch_matches`/`fetch_scorecard`). Match-listen arver
|
||
blind draw-skjuling (ADR-013) automatisk via `own_team_ids()` gjort
|
||
null-sikker for en anonym leser (tom mengde = kun avslørte matcher).
|
||
Scorekortet har en eksplisitt reveal-sjekk i tillegg. Ny `/t/[id]/live`-
|
||
side (leaderboard-bar, øktliste, matcher, hull-for-hull-scorekort),
|
||
lenket fra hovedsiden. Bruker valgte å bygge fullt scorekort med i denne
|
||
runden (utover opprinnelig anbefaling om kun leaderboard+matchliste).
|
||
- Se ARCHITECTURE_DECISIONS.md ADR-023 (kaptein/deltaker) og ADR-026
|
||
(tilskuer) for alle delbeslutningene, og CLAUDE.md-status for
|
||
scratch-verifiseringen (15 + 16 automatiserte sjekker).
|
||
|
||
### Scoring-autorisasjon: hvem fører, hvem korrigerer, hvem lukker (rejst 2026-07-16)
|
||
- **Status:** ❓ delvis avgjort — direkte oppfølger av «Brukerroller» over.
|
||
- **Hvem fører score i dag (2026-07-19, ADR-023):** kun matchens FAKTISKE
|
||
deltakere, ELLER org-eier/admin — `app/team_authz.py` sin
|
||
`user_is_match_participant`. Byttet fra «noen på laget» samme dag. Se
|
||
«Brukerroller» over for full begrunnelse.
|
||
- **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-status kan nå settes via API — se eget punkt lenger ned.
|
||
- **Walkover/konsesjon — ✅ BYGGET OG LIVE 2026-07-19 (ADR-024).** Løste det
|
||
opprinnelige hullet: en side som aldri stiller nok spillere kan nå
|
||
avsluttes ved at den TAPENDE siden (kaptein, eller org-admin) erklærer
|
||
walkover — ensidig, ingen bekreftelse fra motparten. `POST /orgs/{id}/
|
||
matches/{id}/concede` (én match) og `POST /orgs/{id}/tournaments/{id}/
|
||
concede` (gi opp ALLE ikke-avgjorte matcher laget har, i én operasjon —
|
||
v1 er låst til nøyaktig to lag, så det finnes bare én motstander). Kan
|
||
erklæres uansett hvor mange hull som allerede er registrert (poengmessig
|
||
teller kun seier/tap/delt, ikke marginen) — allerede registrerte hull
|
||
forblir urørt i scorekortet. Se ARCHITECTURE_DECISIONS.md ADR-024.
|
||
Frontend: `session-scorecard.tsx` (gi opp én match) og
|
||
`tournament-detail.tsx` (gi opp resten av turneringen, per lag).
|
||
- **Turnering-status via API — ✅ BYGGET OG LIVE 2026-07-19.** `status` lagt
|
||
til i `TournamentUpdate` (`app/routers/tournaments.py`), settes via det
|
||
eksisterende `PATCH /orgs/{id}/tournaments/{id}` (ekte PATCH-semantikk
|
||
uendret — et utelatt `status`-felt lar verdien stå urørt). `status` er,
|
||
ulikt de andre PATCH-bare feltene, en EKTE Postgres ENUM-type
|
||
(`tournament_status`, migrasjon 001) — den generiske SQL-byggeren i
|
||
`update_tournament` fikk et eksplisitt `::tournament_status`-cast for
|
||
akkurat dette feltet, funnet og løst FØR noe ble antatt riktig (verifisert
|
||
i scratch at castet faktisk trengs). Ingen egen tilstandsmaskin/
|
||
overgangsregler — enhver org-medlem med skrivetilgang kan sette hvilken
|
||
som helst av de fire verdiene, samme tillitsnivå som resten av appen.
|
||
Frontend: `tournament-status-badge.tsx` fikk en ny redigerbar
|
||
`TournamentStatusPicker` (dropdown, optimistisk oppdatering med
|
||
rollback ved feil), koblet inn i `tournament-detail.tsx` sin header.
|
||
- **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.
|
||
- **Fortsatt åpent:** (a) ~~skal score-føring begrenses til faktiske
|
||
matchdeltakere~~ ✅ avgjort/bygget 2026-07-19 (ADR-023, se over). (b) skal
|
||
korrigering kreve motpartens godkjenning, eller er upsert-modellen god nok
|
||
for v1? (c) ~~skal turnering-status kunne settes via API~~ ✅ avgjort/
|
||
bygget 2026-07-19 (se over).
|
||
|
||
### 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).
|
||
**Frontend LIVE 2026-07-18:** `/tournaments/[id]/sessions/[sessionId]`,
|
||
`components/session-blind-draw.tsx`. Ny `DELETE .../matches/{id}/
|
||
participants/{id}` (kunne ikke angre et valg før låsing uten den). Fant
|
||
og fikset et "skriv blindt"-hull: org-admin kunne legge til deltakere på
|
||
et lag de ikke er rostret på (forrige rundes utvidelse), men
|
||
`own_team_ids()` i `app/blind_draw.py` viste dem aldri tilbake før
|
||
reveal — utvidet til samme owner/admin-regel som skrive-siden. Se
|
||
CLAUDE.md-status for full runde, inkl. en tredje, urelatert 500-bug
|
||
(manglende handicap-indeks) funnet og fikset samtidig.
|
||
- Kapteinene låser oppstillingen skjult; matchene avsløres samtidig når begge er
|
||
ferdige.
|
||
- **2026-07-19 (ADR-023):** hvem som FÅR låse et lag er nå kaptein-spesifikt
|
||
(eller org-eier/admin) — se «Brukerroller» over. Med fallback for lag uten
|
||
utpekt kaptein ennå.
|
||
- **Nytt 2026-07-18, ✅ BYGGET (fant og fikset rett før frontend-skjermen):**
|
||
`match_participant.tee_id` er påkrevd, men det fantes INGEN vei til å
|
||
liste EN banes tee-er (selv offisielt importerte), og egendefinerte
|
||
baner har ALDRI hatt noen vei til å FÅ tee-er — et hull som fantes fra
|
||
før ADR-019, ikke noe den innførte. Uten dette kunne en turnering på en
|
||
manuelt navngitt bane aldri få en ekte deltaker på noen match. Ny
|
||
`GET/POST /orgs/{id}/courses/{id}/tees` (kun full_18-rating, offisielle
|
||
baner avviser manuell tee-opprettelse). **Presisert eksplisitt: dette er
|
||
IKKE en teeoff.no-kodeendring** — egendefinerte baner er per definisjon
|
||
utenfor teeoffs katalog, så dette er en ren teecup-intern funksjon.
|
||
**Bevisst utenfor omfang:** hull-/stroke-index-data for egendefinerte
|
||
baner (trengs i SCORING-fasen, ikke blind draw) — egen, senere sak når
|
||
scorekort-skjermen bygges.
|
||
|
||
### 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`).
|
||
- **Nytt 2026-07-18, ✅ BYGGET (forarbeid før scorekort-skjermen):**
|
||
`GET/POST /orgs/{id}/courses/{id}/holes` løser hull-/stroke-index-hullet
|
||
fra tee-runden (kun `stroke`-modus trengte det). `GET scorecard` utvidet
|
||
med `stroke_entries`/`hole_result_entries` (rå registrerte tall, ikke
|
||
bare utledet vinn/tap/delt) — uten dette kunne ikke en gjenlastet
|
||
scorekort-side vise gjeldende tilstand. Verifisert med en full
|
||
`stroke`-runde med ekte handicap-justering på en egendefinert bane.
|
||
- **Frontend LIVE 2026-07-18:** `/tournaments/[id]/sessions/[sessionId]/
|
||
matches/[matchId]`, `components/session-scorecard.tsx`. Ett hull om
|
||
gangen med store steppere/knapper (banebruk i sollys, ikke et regneark
|
||
— matcher UX-visjonen lenger ned i denne fila). Lenket fra blind draw
|
||
sin avslørte visning. Ingen nye backend-hull denne runden.
|
||
|
||
### 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).
|
||
|
||
### Leaderboard (turnering-total)
|
||
- **Status:** ✅ HELT FERDIG 2026-07-18 — backend + frontend LIVE
|
||
(`components/tournament-leaderboard.tsx`, `/tournaments/[id]/
|
||
leaderboard`). Siste skjerm i "bygg i rekkefølgen ting brukes"-serien
|
||
for match-play-flyten (oppsett → program → blind draw → scorekort →
|
||
leaderboard, alle live).
|
||
- Summerer poeng på tvers av ALLE økter/matcher, total + per-økt-delsum.
|
||
**Reelt korrekthetshull designet rundt før bygging:** `match.team_a_id`/
|
||
`team_b_id` er ikke garantert konsistent på tvers av matcher (settes per
|
||
match) — summering skjer derfor KUN på ekte lag-id, aldri på "a"/"b"-
|
||
labelen. Verifisert med et scratch-scenario med en bevisst byttet om
|
||
team_a/team_b i én match — riktig total og per-økt-delsum likevel.
|
||
|
||
### Push-varsler
|
||
- **Status:** 📋 planlagt (infrastruktur)
|
||
- «BREAKING: X vant matchen». PWA push. Egen infrastruktur-bit.
|
||
|
||
### Sanntid (WebSockets)
|
||
- **Status:** ✅ HELT FERDIG, LIVE 2026-07-19 (ADR-025 + ADR-027). Koblet
|
||
til BÅDE meldinger (ADR-025) OG `/t/[id]/live` (leaderboard/matcher/
|
||
scorekort, ADR-027). Ny, rutefri delt modul `app/realtime.py` (unngår
|
||
sirkulær import mellom `messaging.py`/`registration.py`/`scoring.py`/
|
||
`tournaments.py`) — sender et "noe endret seg"-signal (ikke selve
|
||
dataene) hver gang `recompute_and_cache_match_state`/`apply_concession`
|
||
kjører, klienten henter de vanlige REST-endepunktene på nytt. Samme
|
||
in-memory-per-prosess-begrensning som meldinger.
|
||
|
||
### 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.
|
||
|
||
### Flere turneringsformater utover Ryder Cup (reist 2026-07-19)
|
||
- **Status:** 📋 notert, IKKE designet/bygget — trenger egen ADR-runde senere.
|
||
- Brukeren ønsker at TeeCup etter hvert skal støtte flere turneringsformater enn
|
||
dagens rene to-lags Ryder Cup-modell (ADR-011). Fire konkrete eksempler gitt,
|
||
med ulik arkitektonisk konsekvens (fra «passer nesten inn i dag» til «krever
|
||
en helt egen datamodell») — notert ordrett under, ikke forenklet, for at
|
||
reglene skal være presise når dette tas fatt på:
|
||
|
||
**«Københavner»** (engelsk: trolig **"Copenhagen"** — direkte oversettelse,
|
||
brukes noen steder i engelskspråklig golf-litteratur om nettopp dette
|
||
poengsystemet, men usikkert om det er en universelt anerkjent
|
||
standardbetegnelse; verdt å dobbeltsjekke før navnet ev. brukes i UI-et).
|
||
3 spillere, alle mot alle (INGEN lag). 6 poeng fordeles på hvert hull etter
|
||
relativ plassering: vinner alene → 4-2-0. Vinner + de to andre deler →
|
||
4-1-1. To deler beste score, én taper → 3-3-0. Alle likt → 2-2-2. Flest
|
||
poeng totalt etter runden vinner. **Størst arkitektonisk avstand fra i
|
||
dag:** ingen to-siders match i det hele tatt, individuelt felt med
|
||
poeng-per-hull-fordeling — passer ikke inn i `match`/`team_a_id`/
|
||
`team_b_id`-modellen slik den er nå (se merknad ved ADR-011).
|
||
|
||
**«High-low-high»** (4 spillere, 2 lag à 2). Per hull: beste spiller
|
||
(«high») på hvert lag møter hverandre i en del-match, dårligste spiller
|
||
(«low») på hvert lag møter hverandre i en egen del-match — resultatet av
|
||
hele hullet i hovedmatchen avgjøres av disse to del-oppgjørene til sammen.
|
||
Eksempel: hull 1 — spiller A (lag 1) får 3 poeng og slår begge på lag 2
|
||
(høyest slår høyest), spiller B (lag 1) stryker og taper mot lagets
|
||
low-motpart. Hullet blir da delt 1-1 siden «high» vant for lag 1 og «low»
|
||
vant for lag 2. **Arkitektonisk vrien del:** hvem som er «high»/«low» per
|
||
hull avgjøres AV SCORENE selv, etter at de er registrert — ikke satt opp
|
||
på forhånd slik dagens `match_participant`-oppsett (fast rolle/side satt
|
||
ved blind draw) forutsetter.
|
||
|
||
**«Robbins»** (foursome med partnerbytte). De første 6 hullene spilles som
|
||
én foursome-match, deretter bytter alle makker og spiller neste 6 hull som
|
||
en ny foursome-match, og de siste 6 hullene spilles med den tredje/siste
|
||
kombinasjonen — slik at alle har spilt med og mot alle i løpet av runden.
|
||
Seier i en 6-hulls delmatch gir 2 poeng, uavgjort gir 1 poeng, flest poeng
|
||
totalt vinner. **Arkitektonisk konsekvens:** én økt blir egentlig TRE
|
||
sekvensielle del-matcher med roterende partnerskap innad i samme runde —
|
||
dagens modell (én match = ett fast lag-oppsett for hele økten) dekker ikke
|
||
dette direkte.
|
||
|
||
**«Try all»** (2 lag, variant av foursome). Begge spillerne slår egen ball
|
||
fra tee, men BYTTER ball til andreslaget (spiller A slår spiller B sin
|
||
ball og omvendt), og paret velger deretter hvilken av de to ballene som
|
||
skal spilles videre — resten av hullet spilles som ordinær foursome på den
|
||
valgte ballen. **Minst arkitektonisk avstand fra i dag:** sannsynligvis en
|
||
ren spilleregel-variant av eksisterende foursome-format (samme
|
||
poengmodell, bare en annen fremgangsmåte de to første slagene) — trenger
|
||
trolig ikke ny datamodell, bare en presisering i regelverket/UI-teksten.
|
||
|
||
- **Ingenting av dette er designet eller bygget ennå** — kun fanget her slik
|
||
at det ikke går i glemmeboken. Naturlig neste steg når dette tas fatt på:
|
||
vurder de fire hver for seg (ikke som én stor runde), start med «Try all»
|
||
(lavest kostnad) hvis en rask seier er ønskelig, eller med «Københavner»
|
||
hvis en bredere individuell/felt-basert turneringstype uansett skal bygges
|
||
først som fundament for de andre.
|
||
|
||
---
|
||
|
||
## Kommunikasjon — ✅ HELT FERDIG 2026-07-19 (ADR-025)
|
||
|
||
| Del | Status | Notat |
|
||
|---|---|---|
|
||
| Lag-intern chat («det hemmelige rommet») | ✅ LIVE | `/tournaments/[id]/teams/[teamId]/chat`. Ekte privat — kun rostrede spillere, INGEN unntak for org-eier/admin (bevisst avvik fra appens vanlige autorisasjonsmønster). `user_is_rostered_on_team` i `app/team_authz.py`. |
|
||
| Offentlig runde-feed («Banter Board») | ✅ LIVE | Egen seksjon nederst på `/t/[id]`. Lesing gjenbruker `tournament.visibility`-mønsteret (ADR-018) uendret, inkl. anonym tilgang for `public`-synlige turneringer. Posting er strengere: krever innlogging OG org-medlemskap/deltakelse. |
|
||
| Bilder i feed/chat | ✅ LIVE | Med fra start (ikke utsatt) — gjenbruker `app/storage.py` sin MinIO+AVIF-pipeline fra ADR-018 uendret. |
|
||
| Sanntid-levering | ✅ LIVE | WebSockets, ikke polling — løser samtidig det tidligere åpne "sanntid vs. polling"-spørsmålet generelt. In-memory tilkoblingsregister per prosess (kun trygt med dagens ene `teecup_api`-container, se ARCHITECTURE_DECISIONS.md "Åpne spørsmål"). Egen Caddy-rute `/ws/*` rett til `teecup_api`. |
|
||
| Video | 💤 | Fortsatt utsatt, egen ADR om/når etterspurt. |
|
||
| 1-til-1 direktemeldinger | 💤 | Fortsatt utsatt, ikke etterspurt. |
|
||
| Moderering (offentlig feed) | ✅ LIVE | Forfatteren selv, ELLER org-eier/admin, kan slette et innlegg. Lag-chatten har ingen moderering utover forfatteren (rommet er privat, org-admin har uansett ikke lesetilgang). |
|
||
|
||
Se ARCHITECTURE_DECISIONS.md ADR-025 for alle fire hovedbeslutningene og
|
||
CLAUDE.md-status for full byggerunde (datamodell, autorisasjon, scratch-
|
||
verifisering med 20 automatiserte sjekker inkl. reell WebSocket-sanntid,
|
||
og utrulling).
|
||
|
||
---
|
||
|
||
## 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()` på `/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 0–100-prosentfelt til
|
||
0–1-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.
|
||
|
||
**Nytt 2026-07-18, ✅ HELT FERDIG (backend + frontend live):** `PATCH`/
|
||
`DELETE` for økter (`app/routers/tournaments.py`) — kunne tidligere verken
|
||
rettes eller slettes etter opprettelse. `DELETE` kun for tomme økter (409
|
||
hvis den har matcher). `PATCH` dekker enkle felt fritt, pluss en egen,
|
||
forsiktig gren for bane-bytte (finner/flytter tilsvarende tee per allerede
|
||
tillagt deltaker, regner om handicap+matchstatus for hele økten etterpå —
|
||
også for allerede AVGJORTE matcher, bekreftet eksplisitt av bruker). Se
|
||
CLAUDE.md-status for det fulle scenarioet (verifisert med et 10-hulls
|
||
avgjort-match-eksempel) og et urelatert funn (`front_9`/`back_9` +
|
||
`stroke`-modus kan aldri få handicap i dag, siden `tee_rating` alltid kun
|
||
lages med `full_18`-omfang).
|
||
**Rediger-/slett-UI LIVE 2026-07-19** — utvidelse av den eksisterende
|
||
Program-skjermen (ingen ny rute). Fanget en reell regresjon i selve
|
||
V0-eksporten før den ble tatt inn: samme runde hadde utilsiktet fjernet
|
||
bane-feltet fra "Legg til økt"-skjemaet — kun de nye rediger/slett-delene
|
||
ble hentet inn, det ekte opprett-skjemaet urørt. Se CLAUDE.md-status for
|
||
detaljer.
|
||
|
||
---
|
||
|
||
### 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.
|
||
|
||
**Nok en reell bug funnet og fikset samme uke, rapportert fra ekte bruk mot
|
||
`teecup.teeoff.no`:** import av samme teeoff-bane til flere økter (helt
|
||
normalt — flere runder spilles ofte på samme bane) ga en 409-feil i stedet
|
||
for å bare gjenbruke banen, og det lagrede navnet ("Hovedbanen" alene) ga
|
||
ingen måte å se hvilken klubb det gjaldt. Fikset: `POST .../courses/
|
||
official-import` er nå idempotent (gir tilbake eksisterende rad ved
|
||
reimport, sjekket FØR teeoff-kallet), og navnet lagres nå som
|
||
"{anlegg} – {bane}". Ekte produksjonsdata for "De Gamle er Eldst" ryddet
|
||
opp (en økt hadde ved et uhell fått en tom søppel-`custom`-bane — se
|
||
CLAUDE.md-status for full hendelse og rotårsak).
|
||
**Åpent, ikke løst i denne runden:** de to bane-søkeflatene (øverste felt
|
||
= organisasjonens egne baner + "opprett ny"-snarvei, "Hent bane fra
|
||
teeoff"-knappen lenger ned = offisielt søk) er lette å forveksle — det var
|
||
nettopp dette som forårsaket søppel-banen. Vurder en tydeligere UI-
|
||
sammenslåing eller rekkefølge-endring i en senere runde.
|
||
|
||
---
|
||
|
||
## Invitasjonskode, ledende side og projisert stilling — ADR-020
|
||
|
||
Reist av brukeren 2026-07-19, rett etter at kamp-play-flyten var komplett.
|
||
Full design i ARCHITECTURE_DECISIONS.md ADR-020.
|
||
|
||
| Del | Status | Notat |
|
||
|---|---|---|
|
||
| Kort invitasjonskode per turnering, overstyrer visibility | ✅ backend | `tournament.join_code` (migrasjon 011), genereres automatisk ved opprettelse. `GET /public/tournaments/by-code/{code}` + kode-bypass i `registration.py`. Løser "muntlig invitert spiller finner ikke turneringen"-hullet. |
|
||
| `match.leading_side` (strukturert, ikke tekst-parsing) | ✅ backend | Cachet i `recompute_and_cache_match_state` ved hver hull-innsending, eksponert på `MatchOut`. |
|
||
| Projisert stilling (hvis pågående matcher holder seg) | ✅ backend | `TeamStanding.projected_points` + `SessionStanding.projected_points_by_team` i leaderboard-endepunktet. Ikke-avgjorte matcher gir full poengsum til `leading_side`, delt 0,5/0,5 ved "AS". |
|
||
| Login-skjerm: kode-felt, tar deg direkte til turneringen | ✅ LIVE 2026-07-19 | `login-form.tsx` sin `JoinByCode`. `code` tres gjennom `/t/[id]` sin lesing OG registrering. |
|
||
| Turnering-detalj: vis invitasjonskode (kopier-knapp) | ✅ LIVE 2026-07-19 | `tournament-detail.tsx` sin `JoinCodeChip` — henter fra org-ens turneringsliste, ikke et nytt endepunkt. |
|
||
| Leaderboard: to stillingsbarer (faktisk + projisert) | ✅ LIVE 2026-07-19 | `tournament-leaderboard.tsx` sin `SegmentedBar` — fargesegmentert rektangel, ikke tall side om side. Eksisterende tall-scoreboard beholdt som detaljvisning. |
|
||
| Fargekoding av matchlister etter ledende lag | ✅ LIVE 2026-07-19 | `session-blind-draw.tsx` sin `RevealedView`/`RevealSide` — farget toppkant + status-chip, bruker `leading_side` + eksisterende `team.color`, ingen ny fargemodell. |
|
||
| Kode-regenerering (ved lekket kode) | 💤 bevisst utsatt | Ikke bygget denne runden — ingen organisator-vei til å bytte ut en kode ennå. Egen sak hvis etterspurt. |
|
||
|
||
**ADR-020 er dermed helt ferdig** — backend + frontend, alle fire
|
||
del-ønsker levert samme dag. Se CLAUDE.md-status for full runde inkl. en
|
||
reell (og transparent håndtert) passord-eksponeringshendelse underveis.
|
||
|
||
---
|
||
|
||
## Innlogging: sesjon-bug + passord/2FA — ADR-021 (reist 2026-07-19)
|
||
|
||
| Del | Status | Notat |
|
||
|---|---|---|
|
||
| Flere organisasjoner per bruker | ✅ bekreftet allerede dekket | ADR-002 fra dag én. `POST /orgs` har ingen begrensning på antall org-er samme bruker kan eie. Ingen kodeendring — kun bekreftet ved gjennomgang 2026-07-19. |
|
||
| Sesjon holder seg ikke — ny magic-link-kode kreves ved hvert besøk | ✅ FIKSET og LIVE 2026-07-19 | Ikke en cookie-bug (brukerens ekte cookie var korrekt satt, 30 dager, `Secure`/`HttpOnly`). Root cause: `app/page.tsx` sjekket aldri om en gyldig sesjon allerede fantes før den viste innloggingsskjemaet. Fikset med server-side sesjonssjekk + redirect til `/dashboard`. Se CLAUDE.md-status for full diagnose og verifisering. |
|
||
| Passord som valgfritt tillegg til magic-link | ✅ LIVE 2026-07-19 | Argon2id-hashing (ikke bcrypt — unngår 72-byte-trunkering). Verifisert med et ekte passord med mellomrom+æøå+spesialtegn. `POST /auth/login-password`, `/auth/set-password`, `/auth/remove-password`. Passord er ALDRI påkrevd. |
|
||
| 2FA: TOTP eller e-post-engangskode, brukerens eget valg | ✅ LIVE 2026-07-19 | SMS bevisst utenfor omfang (krever betalt leverandør). `POST /auth/2fa/setup/start`+`/confirm`, `/auth/2fa/verify`, `/auth/2fa/disable`. |
|
||
| 2FA påkrevd for org-eier/admin, valgfritt for medlemmer | ✅ LIVE 2026-07-19 | Håndheves ved hver innlogging via en `stage: "pending_2fa"`/`"must_enroll_2fa"`-mellomtilstand i sesjons-JWT-en. Verifisert: en fersk org-eier uten 2FA ble korrekt tvunget inn i oppsett ved neste innlogging. |
|
||
| Frontend: passord-innlogging, 2FA-verifisering/-oppsett, kontoinnstillinger | ✅ LIVE 2026-07-19 | `login-form.tsx` (passord-modus), `two-factor-flow.tsx` (delt mellom login/verify), `/account`. |
|
||
|
||
**ADR-021 er dermed helt ferdig.** Se CLAUDE.md-status for full byggerunde,
|
||
inkl. tre reelle bugs funnet og fikset under scratch-testing.
|
||
|
||
### Organisasjonseierskap: dele, invitere, frasi seg, superadmin — ADR-022
|
||
|
||
Reist rett etter ADR-021. Avdekket et bredere, mer fundamentalt hull enn
|
||
bare "del eierskap": det fantes tidligere INGEN vei til å legge til et
|
||
organisasjonsmedlem etter opprettelse i det hele tatt — kun grunnleggerens
|
||
egen `owner`-rad ble noensinne satt inn.
|
||
|
||
| Del | Status | Notat |
|
||
|---|---|---|
|
||
| Flere eiere per organisasjon | ✅ LIVE | Skjemaet støttet det allerede (ingen unikhetssperre); nå faktisk brukbart via API. |
|
||
| E-post-invitasjon (owner→hvilken som helst rolle, admin→kun member) | ✅ LIVE 2026-07-19 | `POST/GET/DELETE /orgs/{id}/invitations`. Auto-akseptert ved neste innlogging (magic-link ELLER passord), samme mønster som spiller-e-post-kobling (ADR-017). Verifisert: admin som forsøkte å invitere som eier ble korrekt avvist. |
|
||
| Rollestyring + frasi seg eierskap (selvbetjent) | ✅ LIVE 2026-07-19 | `PATCH`/`DELETE /orgs/{id}/memberships/{id}`. «Siste eier»-vern (409 `LAST_OWNER`) OG selv-forfremmelse-vern verifisert eksplisitt i scratch. |
|
||
| Superadmin (manuelt DB-tildelt, ikke selvbetjent) | ✅ LIVE 2026-07-19 | `POST /superadmin/orgs/{id}/memberships`. Verifisert: ikke-superadmin avvist (403), superadmin kan sette eierskap på en org de selv ikke er medlem av, ukjent e-post avvist (404). |
|
||
| Fjernet eiers roster-/spillerdata | ✅ avgjort (Beslutning E) | Forblir urørt — organisasjons-styring ≠ deltakelse-historikk. Bekreftet av bruker 2026-07-18. |
|
||
| Frontend: medlemsstyring/invitasjon | ✅ LIVE 2026-07-19 | `org-members.tsx`, `/orgs/[id]/members`, lenket fra dashbordet. |
|
||
|
||
**ADR-022 er dermed helt ferdig.** Ingen dedikert superadmin-UI bygget
|
||
(bevisst — brukes via API av en betrodd operatør, matcher «sjelden,
|
||
manuelt tildelt makt»-designet).
|
||
|
||
---
|
||
|
||
## Rapporterte hull, blind draw + scorekort (2026-07-19)
|
||
|
||
Fire punkter rapportert av brukeren fra faktisk testing av
|
||
`/tournaments/.../sessions/...` (blind draw-skjermen) og scorekort-skjermen,
|
||
foursome-format. Root cause funnet ved kodegjennomgang for alle fire (det
|
||
fjerde, hcp, ble bekreftet mot EKTE produksjonsdata — ikke gjettet). Alle
|
||
fire er nå ✅ FIKSET OG SCRATCH-VERIFISERT samme dag — punkt 2 ble omdefinert
|
||
etter en presisering fra brukeren (se under), fikk en egen ADR (ADR-029) og
|
||
en reell skjemamigrasjon (`014_tee_gender_to_rating.sql`).
|
||
|
||
1. **✅ FIKSET 2026-07-19: valgt spiller forsvant ikke fra listen over
|
||
velgbare spillere.** Root cause: `AddSlotForm` i
|
||
`components/session-blind-draw.tsx` merket allerede brukte roster-rader
|
||
som `disabled` på selve `<option>`-elementet — et `disabled`
|
||
HTML-`<option>` blir stående synlig (kun gråtonet), forsvinner ikke.
|
||
Fikset: `roster`-listen FILTRERES nå ned til kun ledige spillere før
|
||
den rendres, i stedet for å deaktivere valget.
|
||
|
||
2. **✅ FIKSET OG SCRATCH-VERIFISERT 2026-07-19 (ADR-029), IKKE ENNÅ RULLET
|
||
UT mot ekte data.** Brukeren presiserte at min opprinnelige forståelse
|
||
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 (typisk: noen klubber sloper bevisst
|
||
ikke det lengste utslaget for damer). Bekreftet mot ekte produksjonsdata
|
||
(Tjøme Golfklubb, importert via ADR-019): hvert fysisk utslag ("32",
|
||
"44", "50", "55") lå som TO separate `tee`-rader med SAMME navn, én per
|
||
kjønn — selve konflateringen brukeren påpekte. **Fikset:** `gender`
|
||
flyttet fra `tee` til `tee_rating` (migrasjon
|
||
`014_tee_gender_to_rating.sql`, se ADR-029 for full detalj), tee-valg i
|
||
blind draw er nå HELT automatisk (organisator velger kun fysisk
|
||
utslag, riktig kjønnsrating løses fra spillerens `player.gender`
|
||
server-side — bruker bekreftet dette fremfor et forhåndsutfylt-men-
|
||
overstyrbart alternativ), manglende kjønn/rating avvises tydelig FØR
|
||
innsetting (bruker bekreftet "feil høyt" fremfor stille fallback).
|
||
ADR-019s teeoff-import, manuell tee-opprettelse og bane-bytte-remap
|
||
(`_remap_course`) alle oppdatert til samme modell.
|
||
|
||
3. **✅ FIKSET 2026-07-19: tallvelger (1–9 + utvidbar "10 eller flere" →
|
||
10–19) i stedet for pluss/minus-steppere.** Ny `StrokePicker`-komponent
|
||
i `components/session-scorecard.tsx`, erstatter den gamle ±-stepperen
|
||
helt (`stepStroke`/`Minus`/`Plus` fjernet som død kode). Ren
|
||
frontend-endring.
|
||
|
||
4. **✅ FIKSET 2026-07-19: HCP ble faktisk ALDRI beregnet for
|
||
front_9/back_9-økter — bekreftet ekte kodebug, ikke bare en
|
||
synlighetsmangel.** Diagnostisert presist ved å lese EKTE
|
||
produksjonsdata (read-only, `teecup_db`) for brukerens rapporterte
|
||
testøkt: `hole_config='front_9'`, `format='foursome'`, og ALLE fire
|
||
`match_participant`-radene hadde `course_handicap`/`playing_handicap =
|
||
NULL`. Root cause: `app/handicap.py` sin
|
||
`compute_and_store_side_handicaps` joinet `tee_rating` på ØKTENS
|
||
`hole_config` som ratingens `scope` — men `tee_rating`-rader lages i
|
||
praksis KUN med `scope='full_18'` (ADR-019 Beslutning D + tee-
|
||
endepunktet), og ADR-008 sin allerede etablerte design tilsier nettopp
|
||
dette: course handicap skal ALLTID regnes fra full_18-ratingen, uansett
|
||
øktens hole_config — front/back-9-fordelingen skjer SENERE, ved selve
|
||
slagtildelingen (`allocate_over_played_holes`), ikke ved rating-
|
||
oppslaget. Joinen matchet dermed aldri noen rad for en front_9/back_9-
|
||
økt, og handicap ble stille aldri beregnet — påvirket ALLE formater på
|
||
front_9/back_9-økter, ikke bare foursome. **Fikset:** scope hardkodet
|
||
til `'full_18'`, `hole_config`-parameteren fjernet helt fra
|
||
`compute_and_store_side_handicaps`/`_recompute_session_matches` (var
|
||
død etter fiksen) og de tre kallstedene i `matches.py`/`tournaments.py`.
|
||
**Bekreftet at selve UTREGNINGEN matcher brukerens egen beskrivelse
|
||
nøyaktig** (kombinert course handicap / 2 via `CombinedPercentage(0.5)`,
|
||
laveste side satt til 0 mottatte slag via `match_play_strokes`, resten
|
||
fordelt fra stroke index 1 via `allocate_over_played_holes`) — bugen lå
|
||
i at beregningen ALDRI kjørte for front_9/back_9, ikke i selve formelen.
|
||
**Scratch-verifisert presist:** gjenskapte nøyaktig samme scenario
|
||
(foursome + front_9, hcp 10/20 vs. 5/15) — course/playing handicap
|
||
beregnet korrekt (10/21 vs. 5/16, kombinert 16/10), og et hull med
|
||
IDENTISK bruttoscore (5-5) på begge sider ga et ikke-delt resultat
|
||
("a" vant, ikke "halved") — direkte bevis på at handicap nå faktisk
|
||
brukes. Ingen migrasjon (ren Python-logikk-fiks).
|
||
**Ekte, urelatert bug funnet UNDER samme scratch-test, IKKE fikset
|
||
ennå:** et stroke-modus scoreinnsending på en bane UTEN registrerte
|
||
hull (`hole`-tabellen tom) krasjer med en rå 500
|
||
(`IndexError: list index out of range` i `handicap_engine.py` sin
|
||
`allocate_over_played_holes`, kalt fra `scoring.py` sin
|
||
`_compute_hole_results`) i stedet for en tydelig `VALIDATION_FAILED`.
|
||
Samme klasse feil som den allerede kjente/fikset
|
||
manglende-handicap-indeks-krasjen fra blind draw-runden (2026-07-18) —
|
||
bør fikses likt (eksplisitt sjekk FØR beregning, ikke en try/except
|
||
rundt symptomet). Ikke fikset i denne runden, kun oppdaget og notert.
|
||
|
||
**Designspørsmålene for punkt 2 er avklart** (bruker valgte det anbefalte
|
||
alternativet på begge, se ADR-029 Beslutning B/C): helautomatisk tee-valg
|
||
(ingen manuell kjønnsvelger), og "feil høyt" ved manglende kjønn/rating
|
||
(ingen stille fallback). Migrasjon 014 er skrevet og scratch-verifisert,
|
||
men IKKE kjørt mot ekte `teecup_db` ennå — venter på eksplisitt
|
||
brukerbekreftelse (endrer ekte Tjøme-data: slår sammen 8 tee-rader til 4).
|
||
|
||
---
|
||
|
||
## Rapporterte UI-/UX-hull (2026-07-19) — notert, IKKE fikset ennå
|
||
|
||
Fire punkter rapportert av brukeren fra faktisk bruk av `teecup.teeoff.no`.
|
||
Root cause funnet ved kodegjennomgang for de tre første (ikke bare gjettet);
|
||
det fjerde er et reelt manglende UI-element, ingen kodefeil. Ingen av de fire
|
||
er fikset i denne runden — kun dokumentert slik at de ikke går i glemmeboken.
|
||
|
||
1. **Dashboard-turneringskortet viser "Ingen datoer satt" selv om øktene
|
||
(rundene) har dato/klokkeslett satt.** `components/tournament-card.tsx`
|
||
leser `tournament.start_date`/`end_date` — et EGET, frittstående felt på
|
||
selve turneringen (ADR-015), atskilt fra `session.scheduled_at` (dato per
|
||
økt/runde). Grep bekrefter: disse to tournament-feltene LESES tre steder
|
||
i frontend (`dashboard.tsx`, `public-tournament.tsx`, `public-club.tsx`),
|
||
men skrives INGEN steder — det finnes ingen UI for å sette dem i det hele
|
||
tatt. Kortet vil derfor alltid vise "ingen datoer satt", uansett hvor mange
|
||
økter som har fått en `scheduled_at`, fordi feltet det leser aldri kan bli
|
||
satt gjennom UI-et. To mulige retninger: (a) bygg et faktisk
|
||
`start_date`/`end_date`-skjemafelt på turneringen, eller (b) la kortet
|
||
utlede visningsdatoen fra øktenes `scheduled_at`-spenn i stedet for et
|
||
eget, separat felt — sistnevnte er trolig det organisatoren faktisk
|
||
forventer.
|
||
|
||
2. **`/orgs/{id}/members`-siden gir en rå API-404 (`{"detail":"Not
|
||
Found"}`), ikke medlemssiden.** Bekreftet root cause, ikke bare
|
||
reprodusert: `frontend/next.config.mjs` sin `rewrites()` returnerer en
|
||
PLAIN ARRAY (implisitt "afterFiles"-semantikk i Next.js) — det betyr at
|
||
ikke-dynamiske filer/sider sjekkes FØR rewrites, men DYNAMISKE sider
|
||
(som `app/orgs/[id]/members/page.tsx`) sjekkes ETTER. Rewrite-regelen
|
||
`{ source: "/orgs/:path*", destination: ".../orgs/:path*" }` (satt opp i
|
||
ADR-016 for å proxye API-kall) fanger derfor `/orgs/{id}/members` FØR
|
||
Next.js noensinne når frem til den faktiske siden, og sender kallet til
|
||
FastAPI i stedet — som naturligvis ikke har noen `GET /orgs/{id}/members`-
|
||
rute (kun `/orgs/{id}/memberships`), derav den rå FastAPI-404-formen
|
||
(ikke engang appens egen `app_error`-kontrakt, siden ruten ikke matcher
|
||
noe sted i det hele tatt). Selve siden (`org-members.tsx`, lenken fra
|
||
dashbordet) er ellers riktig bygget — dette er en ren
|
||
rewrite/dynamisk-rute-presedens-krasj, samme klasse fallgruve som
|
||
ADR-016 sin opprinnelige "alt nytt API-prefiks må inn i rewrites"-lærdom,
|
||
bare i motsatt retning (en frontend-SIDE ble skjult AV en rewrite). Dette
|
||
er den FØRSTE frontend-siden som noensinne har blitt nestet direkte under
|
||
et allerede proxyet prefiks (`/orgs/*`) — ingen tidligere skjerm har
|
||
truffet dette. Sannsynlig fiks: flytt siden til en ikke-proxyet sti
|
||
(f.eks. `/organizations/[id]/members`), ELLER gjør rewrites-regelen mer
|
||
presis (kun kjente API-undermønstre som `/orgs/:id/tournaments`,
|
||
`/orgs/:id/memberships` osv., ikke et bredt `:path*`).
|
||
|
||
3. **Dashboard: ingen vei til å opprette/legge til en ANDRE organisasjon.**
|
||
Bekreftet i `dashboard.tsx`: `CreateOrganizationState`
|
||
(opprett-organisasjon-skjemaet) vises KUN når `hasOrg` er `false`, altså
|
||
når brukeren har null organisasjoner fra før. Har brukeren allerede én
|
||
org, finnes ingen knapp/lenke noe sted i UI-et for å opprette en til —
|
||
selv om backend-et støtter dette fullt ut og uten begrensning (ADR-021,
|
||
bekreftet: `POST /orgs` har ingen grense på antall org-er én bruker kan
|
||
eie). Ren manglende UI, ikke en backend-begrensning.
|
||
|
||
4. **Ingen måte å se, på ett blikk, at ALLE runder/økter i en turnering har
|
||
fått dato/klokkeslett satt.** `tournament-program.tsx` viser
|
||
`scheduled_at` per øktkort hvis satt, ingenting spesielt (ingen
|
||
fremhevet "mangler dato"-tilstand) hvis ikke. Ingen sammendrag/telling
|
||
noe sted ("X av Y runder har dato") — organisatoren må åpne
|
||
program-skjermen og lese hvert kort manuelt. Ren UX-mangel, ingen
|
||
bakenforliggende datamodell-begrensning (all nødvendig data finnes
|
||
allerede i `GET .../sessions`).
|
||
|
||
**Ingenting av dette er fikset ennå** — kun diagnostisert og notert på
|
||
brukerens eksplisitte instruks, for å ikke gå i glemmeboken mens PWA-runden
|
||
prioriteres.
|
||
|
||
---
|
||
|
||
## UX / frontend (senere fase)
|
||
|
||
- 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp
|
||
(banebruk i sollys/med solbriller).
|
||
- ✅ Offline-first (ADR-006) — BYGGET 2026-07-19 (ADR-028), se eget punkt
|
||
under. Scoreregistrering (hole-scores/hole-results) fungerer nå offline
|
||
med automatisk synk.
|
||
- ✅ PWA: manifest, service worker, «Legg til på hjemskjerm» — BYGGET
|
||
2026-07-19 (ADR-028).
|
||
|
||
### PWA — ✅ BYGGET OG LIVE 2026-07-19 (ADR-028)
|
||
|
||
Full design i ARCHITECTURE_DECISIONS.md ADR-028. Kort:
|
||
|
||
| Del | Status | Notat |
|
||
|---|---|---|
|
||
| Installerbar app (manifest + ikoner + «Legg til på hjemskjerm») | ✅ bygget | `app/manifest.ts` (Next.js sin innebygde manifest-generator), `components/sw-register.tsx`, `appleWebApp`-metadata for iOS. |
|
||
| Ikoner | ✅ bygget, **MIDLERTIDIG** | Enkelt grønt golf-flagg generert programmatisk (`public/icons/*`, `public/apple-icon.png`) — **skal erstattes med ekte design senere.** Erstattet samtidig den gamle v0.app-plassholderlogoen som lå i `apple-icon.png` fra før (var aldri TeeCup-merkevare). |
|
||
| Service worker: cache app-navigasjon + `/orgs/*`-GET-er | ✅ bygget | `public/sw.js`, nettverk-først/cache-fallback (bevisst IKKE stale-while-revalidate, se ADR-028). `public/offline.html` som siste utvei. |
|
||
| Offline scoreregistrering (hole-scores/hole-results) | ✅ bygget | `lib/offline-queue.ts` (IndexedDB-kø) + `components/session-scorecard.tsx`. Synker automatisk ved `window`s `online`-event, pluss manuell "Synkroniser nå"-knapp. Bevisst IKKE Background Sync API (iOS Safari støtter den ikke). |
|
||
| Andre skrivehandlinger offline (walkover, chat/feed, oppsett) | 💤 bevisst utenfor omfang | Kun de to scoreregistrerings-endepunktene er køet — se ADR-028 Beslutning B for begrunnelse per type. |
|
||
| Faktisk browser-testet (DevTools Offline-modus) | ❌ **FORTSATT IKKE GJORT — OPPFØLGINGSPUNKT** | Kun verifisert med typesjekket build + container-boot/curl, aldri i en ekte nettleser. Ingen nettleserverktøy tilgjengelig i byggeøkten. **Brukeren bør selv åpne et scorekort, skru på Chrome DevTools sin Offline-bryter, registrere et par slag, skru nettet på igjen, og bekrefte at de faktisk synkes** — først da er offline-flyten reelt bevist, ikke bare kodegjennomgått. |
|
||
|
||
**Rullet ut live 2026-07-19**, bruker bekreftet eksplisitt: `docker compose
|
||
up -d --build teecup_frontend` (ingen migrasjon). Verifisert:
|
||
`/health`/`dashboard` → 200, `/manifest.webmanifest`/`sw.js`/ikoner alle 200
|
||
over ekte https, `teeoff.no` upåvirket.
|
||
|
||
---
|
||
|
||
## 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).
|