Live og verifisert ende-til-ende: /verify-siden bygget og live, e-postmalen sender nå en ekte klikkbar lenke (app/email.py/app/config.py) Frontend containerisert (frontend/Dockerfile, standalone Next.js) og rullet ut som egen teecup_frontend-tjeneste Caddy peker nå teecup.teeoff.no på frontend-en, som selv proxyer API-kall server-side — same-origin, ingen CORS, cookie uendret (dokumentert som ny ADR-016) Fant og fikset en reell fallgruve: Next.js sin rewrites() bakes inn ved build-tid for standalone-output, ikke lest ved kjøretid — løst med en Docker build-time ARG Ekte e-post sendt, ekte lenke klikket, sesjon opprettet — bekreftet av deg Én ting du må huske: Caddy-endringen ligger i det separate /opt/teeoff-repoet (deploy/Caddyfile), ikke i teecup-repoet — den er ikke committet ennå. Lett å glemme siden resten av denne økten kun har jobbet i /opt/teecup. Vil du at jeg minner deg, eller committer du den nå selv i teeoff-repoet? CLAUDE.md, FEATURE_BACKLOG.md og ARCHITECTURE_DECISIONS.md (ny ADR-016) er oppdatert. Klar for commit i teecup-repoet når du vil.
417 lines
26 KiB
Markdown
417 lines
26 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. Endret fra Geminis «delt database direkte». |
|
|
| 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: 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)
|
|
- **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.
|
|
|
|
### 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å ikke besluttet (deltakere/org/offentlig lenke). |
|
|
| 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. |
|
|
|
|
---
|
|
|
|
## 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).
|