Flytte is_participant-logikk til team_authz.py (unngå sirkulær import) Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup Widen tournaments.py: list_sessions/list_teams/concede_tournament Widen courses.py: list_holes Widen messaging.py: team chat REST-endepunkter (list/send/delete) Legge til my_session_id/my_match_id i /auth/me sin my_tournaments Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder» Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller) Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag Presentere plan, deploye og oppdatere .md-filer for deltaker-tilgang-runden Bygge og scratch-verifisere sekundær e-postadresse (migrasjon 017) Designe, bygge og scratch-verifisere HCP-historikk (migrasjon 018) HCP-historikk er bygget og scratch-verifisert (18/18 sjekker, test_isolation.sql fortsatt 12/12). Hva er bygget: Migrasjon 018_handicap_history.sql — ny append-only-tabell handicap_history, kun for den personlige profilens HCP (ikke lagenes frosne handicap_index_snapshot, som er urørt, ADR-007). Backend: PATCH /auth/profile logger nå automatisk en ny rad når HCP faktisk endres til en tallverdi — ikke ved ren nullstilling, og ikke ved gjentatt lagring av samme verdi (sjekket eksplisitt mot forrige verdi først). Ny GET /auth/profile/handicap-history. Frontend: en «Vis HCP-historikk»-lenke i /account sin profilseksjon, viser dato + verdi-liste, oppdateres automatisk etter lagring. Verifisert grundig: ingen duplikat ved gjentatt lagring av uendret verdi, korrekt logging ved faktisk endring, ingen logg ved nullstilling, ny logg ved gjeninnsetting etter nullstilling, kronologisk rekkefølge riktig, og full isolasjon mellom to brukeres historikk.
1472 lines
95 KiB
Markdown
1472 lines
95 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.
|
||
- **Ressurs, lagt til 2026-07-19:** brukeren har lastet opp tre PDF-er til
|
||
prosjektroten (`spilletyper-og-spilleformer-2023.pdf`, `Live Tourney _ A
|
||
Guide to Handicap Scoring in Golf for Tournaments.pdf`, `SCGA Club
|
||
Digest.pdf`) som til sammen skal gi en tydelig beskrivelse av hvordan HCP
|
||
og mottatte/tildelte slag beregnes/fordeles — les disse FØR design av
|
||
handicap-/slagfordelingslogikken for disse fire formatene, se CLAUDE.md
|
||
sin "Autoritative kilder"-seksjon.
|
||
|
||
---
|
||
|
||
## 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 LIVE 2026-07-19 (ADR-029).** 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 kjørt mot ekte `teecup_db`
|
||
2026-07-19, bruker bekreftet eksplisitt — Tjømes 8 tee-rader slått sammen
|
||
til 4, alle referanser intakte.
|
||
|
||
**Alle fire punkter rullet ut live 2026-07-19**, bruker bekreftet
|
||
eksplisitt: migrasjon 014 + `docker compose up -d --build teecup_api
|
||
teecup_frontend`. Verifisert: `/health`/`dashboard` → 200, `teeoff.no`
|
||
upåvirket, 0 brutte `match_participant.tee_id`-referanser etter
|
||
sammenslåingen.
|
||
|
||
---
|
||
|
||
## 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. **✅ FIKSET 2026-07-19/20 (ADR-030).** Dashboard-turneringskortet viste
|
||
"Ingen datoer satt" selv om øktene (rundene) hadde dato/klokkeslett
|
||
satt — bekreftet av brukeren med et faktisk skjermbilde (en økt tydelig
|
||
planlagt til "lør. 11. juli", men kortet viste fortsatt "Ingen datoer
|
||
satt"). Root cause: `tournament.start_date`/`end_date` var et EGET,
|
||
frittstående felt (ADR-015) som INGEN UI noensinne satte — helt atskilt
|
||
fra `session.scheduled_at`. Valgte retning (b) fra de to opprinnelig
|
||
skisserte alternativene: `GET /orgs/{id}/tournaments` utleder nå
|
||
datospennet fra øktenes `scheduled_at` (COALESCE med et evt. eksplisitt
|
||
satt `start_date`/`end_date`, som fortsatt vinner om det noensinne
|
||
settes). Se ADR-030 for full detalj og verifisering.
|
||
|
||
2. **✅ FIKSET 2026-07-20: `/orgs/{id}/members`-siden ga 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)
|
||
— statiske filer/sider sjekkes FØR rewrites, men DYNAMISKE sider (som
|
||
`app/orgs/[id]/members/page.tsx`) sjekkes ETTER. Rewrite-regelen
|
||
`{ source: "/orgs/:path*", ... }` (ADR-016) fanget derfor kallet FØR
|
||
Next.js noensinne nådde selve siden, og sendte det til FastAPI i
|
||
stedet (som naturligvis ikke har noen `GET /orgs/{id}/members`-rute).
|
||
**Fikset:** siden flyttet til `app/organizations/[id]/members/page.tsx`
|
||
(utenfor det proxyede `/orgs/*`-prefikset), eneste lenke til den
|
||
(`dashboard.tsx`) oppdatert tilsvarende. Lagt til en forklarende
|
||
kommentar direkte i `next.config.mjs` sin `rewrites()` slik at samme
|
||
feil ikke gjentas for en fremtidig ny side. Verifisert med ekte
|
||
produksjonsbuild + container-boot: den nye ruten rendrer faktisk
|
||
`OrgMembers`-komponenten (ikke en 404 eller innloggingssiden).
|
||
|
||
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`).
|
||
|
||
Punkt 1 (datovisning, ADR-030) og punkt 2 (medlemsside-ruten) er nå ✅
|
||
fikset, se over.
|
||
|
||
3. **✅ FIKSET 2026-07-21: ingen vei til å opprette en ANDRE organisasjon.**
|
||
Ny `NewOrganizationControl`-komponent i `dashboard.tsx` (identisk mønster
|
||
som `NewTournamentControl` — inline-ekspanderende navnefelt), plassert i
|
||
`OrganizationView` sin header ved siden av «Medlemmer»/«Ny turnering»,
|
||
uavhengig av antall org-er brukeren allerede har. Ingen backend-endring
|
||
nødvendig (`POST /orgs` hadde aldri noen begrensning).
|
||
|
||
4. **✅ FIKSET 2026-07-21: intet sammendrag for "alle runder har dato".**
|
||
Ny `DateCoverageSummary`-komponent i `tournament-program.tsx`, vist
|
||
øverst i øktlisten (kun når minst én økt finnes): «X av Y runder har
|
||
fått dato og klokkeslett» (uthevet/grønn når alle er satt). Ren
|
||
klientside-telling av allerede lastet `scheduled_at`-data, ingen
|
||
backend-endring.
|
||
|
||
Alle fire punktene fra 2026-07-19 er dermed fikset. Begge siste fikser
|
||
verifisert med ekte typesjekket produksjonsbuild, rullet ut live
|
||
2026-07-21 (kun `teecup_frontend`, ingen migrasjon), bruker bekreftet
|
||
eksplisitt.
|
||
|
||
---
|
||
|
||
### Rediger spiller (spillerpool) — ✅ BYGGET OG SCRATCH-VERIFISERT 2026-07-19/20
|
||
|
||
Reist av brukeren samme runde som ADR-030: ingen vei fantes til å rette en
|
||
feilregistrert spiller (f.eks. en HCP-skrivefeil) etter opprettelse — kun
|
||
`POST` fantes for `player`.
|
||
|
||
Ny `PATCH /orgs/{id}/players/{id}` (`app/routers/players.py`), vanlig
|
||
`exclude_unset`-PATCH-mønster, dekker alle spillerfelt (navn, HCP, kjønn,
|
||
mobil, e-post, fødselsdato, kallenavn, land, klubb, medlemsnummer). Frontend:
|
||
ny "Rediger spiller"-handling i rosterradens "⋮"-meny
|
||
(`tournament-detail.tsx`), inline skjema for navn/HCP/kjønn.
|
||
|
||
**Viktig, bevisst grense — kommunisert i selve UI-et, ikke skjult:** dette
|
||
endrer spilleren i POOLEN (brukes ved fremtidig rostring), IKKE et lags
|
||
allerede FROSNE `team_roster.handicap_index_snapshot` (ADR-007 — reproduser-
|
||
barhet for allerede opprettede turneringer/lag er et bevisst designvalg, ikke
|
||
noe denne rundens fiks endrer på). Skjemaet viser en tydelig forklarende
|
||
tekst om dette; å oppdatere et allerede rostret lags viste HCP-tall krever
|
||
fortsatt å fjerne og legge til spilleren på nytt (eksisterende funksjon).
|
||
|
||
**Scratch-verifisert:** HCP-endring lykkes, delvis PATCH (kun navn) lar HCP
|
||
stå urørt, tomt PATCH avvist (400), ukjent spiller-id gir 404, og — den
|
||
kritiske sjekken — en spillers allerede frosne roster-snapshot for et
|
||
EKSISTERENDE lag forble uendret etter en påfølgende spiller-PATCH (bekrefter
|
||
ADR-007 fortsatt holder). Ekte typesjekket produksjonsbuild kjørt og
|
||
bekreftet.
|
||
|
||
**Rullet ut live 2026-07-20**, bruker bekreftet eksplisitt, ingen migrasjon.
|
||
|
||
---
|
||
|
||
## Personlig landingsside for ENHVER registrert bruker + personlig profil — ✅ BYGGET OG SCRATCH-VERIFISERT 2026-07-20 (ADR-031)
|
||
|
||
Reist av brukeren 2026-07-20 som en "tenk igjennom og foreslå"-instruks,
|
||
deretter et "gjør det" med utvidet omfang (personlig profil-CRUD lagt til:
|
||
profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb). Full
|
||
detalj i ARCHITECTURE_DECISIONS.md ADR-031 — kort her:
|
||
|
||
| Del | Status | Notat |
|
||
|---|---|---|
|
||
| Ett samlet dashboard (ikke to atskilte ruter) | ✅ | Ny "Mine runder"-seksjon øverst i `dashboard.tsx`, organisasjonsseksjonen uendret under, begge vises hvis begge finnes. |
|
||
| "Mine runder" — tverr-org spiller-oppslag | ✅ | Ny `player_organizations_for_user()`-bro (migrasjon 015, samme mønster som fire tidligere), `/auth/me` utvidet med `my_tournaments`. Kun rostrede lag i v1 (ikke rene påmeldinger uten roster). |
|
||
| Personlig profil (fornavn/etternavn/fødselsdato/kjønn/HCP/hjemmeklubb) | ✅ | Nye felt på `app_user` (migrasjon 015) — ETT sett per konto, BEVISST atskilt fra org-scopede `player`-rader (se ADR-031 Beslutning B for hvorfor). `PATCH /auth/profile`, ny seksjon i `/account`. |
|
||
| Profilbilde | ✅ | `POST`/`DELETE /auth/profile/avatar`, samme ekte multipart→AVIF-opplasting som turnering-hero-bilder (ADR-018). |
|
||
| **Sikkerhetsutvidelse funnet UNDER bygging:** deltaker-tilgang uansett synlighetsnivå | ✅ | `check_visibility()` ga tidligere kun deltaker-tilgang for `visibility='participants'` — IKKE for `'org'` (DEFAULT for enhver ny turnering), som ville stengt ute enhver spiller uten org-medlemskap fra sin EGEN turnering. Utvidet til å gjelde begge ikke-offentlige tier. Verifisert: deltaker FÅR nå tilgang, fremmed+anonym fortsatt avvist (ingen innstramming, ren utvidelse). |
|
||
|
||
**Bevisst UTENFOR omfang, kjent gjenstående begrensning (se ADR-031 for full
|
||
begrunnelse):** "Mine runder" lenker til den offentlige turnering-siden
|
||
(`/t/{id}`), IKKE til lagets private chat eller scorekortet — disse krever
|
||
fortsatt ekte organisasjonsmedlemskap (`get_authorized_org`), en strengere
|
||
sperre enn deltaker-status alene, brukt av dusinvis av endepunkter på tvers
|
||
av appen. Å utvide DEN sperren til også å godta "faktisk deltaker" er en
|
||
egen, større og mer risikofylt endring (påvirker autorisasjonsarkitekturen
|
||
bredt, ikke ett enkelt endepunkt) — naturlig neste steg, men bevisst ikke
|
||
gjort i denne runden. Notifikasjons-/aktivitetsfeed og HCP-historikk over
|
||
tid også bevisst utenfor omfang v1 (samme begrunnelse som opprinnelig
|
||
forslag).
|
||
|
||
**Oppdatering 2026-07-21 — deltaker-tilgang til lag-chat/scorekort er
|
||
dermed ✅ BYGGET OG LIVE, se egen seksjon lenger ned.**
|
||
|
||
**Scratch-verifisert, 15 sjekker** (profil-CRUD komplett, inkl. sletting av
|
||
enkeltfelt og avatar; en EKTE ren spiller uten org-medlemskap ser riktig
|
||
`my_tournaments`; den kritiske sikkerhetssjekken: samme spiller får nå se
|
||
sin `'org'`-synlige turnering, mens fremmed/anonym fortsatt avvises). Ekte
|
||
typesjekket produksjonsbuild kjørt og bekreftet.
|
||
|
||
**Rullet ut live 2026-07-20**, bruker bekreftet eksplisitt: migrasjon 015
|
||
kjørt mot ekte `teecup_db`, begge containere redeployet, `/health`/
|
||
`/dashboard`/`/account` → 200, `teeoff.no` upåvirket.
|
||
|
||
### Naturlig neste steg (ikke bygget, notert for senere)
|
||
- **Deltaker-tilgang (uten org-medlemskap) til lag-chat og scorekort** — det
|
||
gjenstående hullet nevnt over. Krever en egen, forsiktig gjennomgang av
|
||
`get_authorized_org`-bruken (brukt bredt i hele appen) — ikke en rask fiks.
|
||
- Notifikasjons-/aktivitetsfeed på "Mine runder".
|
||
- HCP-historikk over tid (personlig profil sin `handicap_index` endres i
|
||
dag uten noen logg).
|
||
- "Mine runder" for RENE påmeldinger (`tournament_registration` uten
|
||
roster ennå) — v1 viser kun rostrede lag.
|
||
|
||
### Oppfølging samme dag: e-post + mobil — ✅ BYGGET OG SCRATCH-VERIFISERT (ADR-032)
|
||
|
||
Brukeren påpekte rett etter forrige runde at "identifikatoren" (e-post)
|
||
manglet i profilen, og at mobil (med landsnummer) burde være en opsjon. Full
|
||
detalj i ADR-032.
|
||
|
||
| Del | Status | Notat |
|
||
|---|---|---|
|
||
| Mobil (landsnummer + nummer, to separate felt) | ✅ | Del av samme `PATCH /auth/profile` som resten av profilen — ren tilføyelse. |
|
||
| E-post — verifisert to-stegs bytte, IKKE en enkel PATCH | ✅ | Ny `email_change_token`-tabell (migrasjon 016, samme mønster som `magic_link_token`). `POST /auth/profile/email` (krever sesjon, sender lenke til den NYE adressen) + `POST /auth/profile/email/confirm` (forbruker token atomisk, ingen sesjon påkrevd — samme som selve magic-link-verifiseringen). E-posten endres ALDRI før lenken faktisk åpnes. Ny `/verify-email`-side. |
|
||
|
||
**Scratch-verifisert, 10 sjekker** — inkl. at et bytte til en allerede brukt
|
||
adresse avvises, at e-posten forblir uendret helt til bekreftelse, at samme
|
||
kode ikke kan gjenbrukes, og at en ny innlogging med den GAMLE adressen
|
||
oppretter en fersk, tom konto (beviser byttet er reelt, ikke kosmetisk).
|
||
Ekte typesjekket produksjonsbuild kjørt og bekreftet.
|
||
|
||
**Rullet ut live 2026-07-20**, bruker bekreftet eksplisitt: migrasjon 016
|
||
kjørt mot ekte `teecup_db`, begge containere redeployet, `/health`/
|
||
`/dashboard`/`/account`/`/verify-email` → 200, `teeoff.no` upåvirket.
|
||
|
||
---
|
||
|
||
## Midlertidige spillere + automatisk etter-runde-invitasjon — 📋 FORESLÅTT 2026-07-20, IKKE bygget ennå
|
||
|
||
Reist samme runde som punktet over, uttalt som punkt 2 (ikke like prioritert
|
||
som "Mine runder"-dashbordet, men skal likevel dokumenteres grundig nå).
|
||
|
||
**Viktig presisering, funnet ved kodegjennomgang FØR noe ble antatt:** det
|
||
meste av "midlertidig spiller"-behovet er allerede dekket av eksisterende
|
||
funksjonalitet, ikke et hull i seg selv — `POST /orgs/{id}/players` krever
|
||
ALDRI at spilleren har en konto (`player.user_id` er nullable, kobles først
|
||
automatisk når/hvis noen logger inn på matchende e-post, ADR-017
|
||
Beslutning B). En organisator kan altså allerede legge til "Ola Nordmann,
|
||
ola@example.com" uten at Ola noensinne har hørt om TeeCup. Det som
|
||
FAKTISK mangler er den PROAKTIVE oppfølgingen brukeren ber om: et
|
||
automatisk e-post-utsendelse-steg etter runden, med scorekort + invitasjon
|
||
til å logge inn og "ta eierskap" over spiller-profilen sin — dette finnes
|
||
ikke i noen form i dag (en spiller må selv, uoppfordret, logge inn for at
|
||
koblingen skal skje).
|
||
|
||
**Foreslått design, IKKE bygget:**
|
||
- **Utløses av en EKSPLISITT organisator-handling, ikke en automatisk
|
||
bakgrunnsjobb** ("Send scorekort og invitasjon til alle med e-post i
|
||
denne økten", en knapp på øktnivå når øktens matcher er avgjort).
|
||
Anbefalt fremfor helautomatisk utsendelse ved et gjettet
|
||
"runden er ferdig"-tidspunkt — unngår uventede e-poster fra en
|
||
feilaktig auto-deteksjon, og matcher prosjektets øvrige mønster (blind
|
||
draw krever eksplisitt lås, walkover er en eksplisitt handling — ingen
|
||
"magisk" auto-trigger noe annet sted i appen).
|
||
- **Ingen ny databasekolonne nødvendig for selve "midlertidig"-begrepet**
|
||
— enhver `player`-rad UTEN `user_id` ER allerede "midlertidig" i praksis.
|
||
Kun en NY, liten `sent_at`-lignende sporingskolonne kan trengs for å
|
||
unngå dobbel utsending ved gjentatt klikk (åpent spørsmål, se under).
|
||
- **E-posten gjenbruker eksisterende infrastruktur** (`app/email.py`,
|
||
samme SMTP-oppsett som magic-link/2FA) — innhold: spillerens
|
||
hull-for-hull-resultat for økten + en ekte innloggingslenke (vanlig
|
||
magic-link, ingen ny auth-mekanisme nødvendig siden `link_player_by_
|
||
email()` allerede kobler kontoen automatisk ved første innlogging).
|
||
|
||
**Åpne spørsmål, trengs FØR bygging:**
|
||
- Skal utsendingsknappen ligge på ØKT-nivå (send til alle i denne ene
|
||
runden) eller TURNERING-nivå (send til alle på tvers av alle økter, når
|
||
hele turneringen er ferdig)? Økt-nivå virker riktigst — en spiller kan
|
||
ha spilt kun én av flere økter.
|
||
- Skal systemet spore "allerede sendt til denne spilleren for denne økten"
|
||
for å hindre dobbel utsending ved et nytt klikk (sannsynligvis ja — én
|
||
liten ny tabell/kolonne)?
|
||
- Skal e-posten sendes på spillerens/organisasjonens foretrukne språk
|
||
(samme `locale`-mønster som magic-link-e-posten, ADR-015 Beslutning C)?
|
||
|
||
---
|
||
|
||
## Deltaker-tilgang til lag-chat og scorekort (uten org-medlemskap) — ✅ BYGGET OG LIVE 2026-07-21
|
||
|
||
Direkte oppfølging av ADR-031s kjente, notert begrensning: "Mine runder"
|
||
lenket til den offentlige turnering-siden, men IKKE til lagets private
|
||
chat eller det skrivbare scorekortet — begge krevde fortsatt ekte
|
||
organisasjonsmedlemskap (`get_authorized_org`), noe en ren, rostret/
|
||
påmeldt spiller (uten organisasjonsmedlemskap) ikke har.
|
||
|
||
**Kjernefunn ved gjennomlesing (ikke antatt):** de faktiske
|
||
autorisasjonsprimitivene (`user_is_rostered_on_team`,
|
||
`user_is_match_participant`, `user_is_team_captain`,
|
||
`own_team_ids` — alle i `app/team_authz.py`/`app/blind_draw.py`) støttet
|
||
ALLEREDE ikke-org-medlemmer korrekt overalt — de var bare plassert BAK en
|
||
ekstra, blank `Depends(get_authorized_org)`-sperre på ni endepunkter på
|
||
tvers av fire filer. Fikset ved kirurgisk å fjerne akkurat den sperren fra
|
||
disse ni (lag-chat lese/skrive/slette i `messaging.py`; scorekort-lesing,
|
||
slag-/hull-resultat-innsending, walkover i `scoring.py`; match-/lag-/
|
||
økt-listing i `matches.py`/`tournaments.py`; walkover-på-turnering-nivå i
|
||
`tournaments.py`; bane-hull i `courses.py`) — de eksisterende
|
||
domene-sjekkene (som allerede har egen org-admin-fallback der det er
|
||
tiltenkt) er den REELLE sikkerhetsgrensen, ikke `get_authorized_org`.
|
||
|
||
**For endepunkter som IKKE hadde noen finkornet sjekk i det hele tatt**
|
||
(f.eks. `get_scorecard`, `list_sessions`, `list_teams` — disse stolte
|
||
UTELUKKENDE på org-medlemskap) ville en ren fjerning av sperren latt EN
|
||
HVILKEN SOM HELST innlogget bruker se dem — løst med et nytt, eksplisitt
|
||
`is_org_member(...) OR user_is_tournament_participant(...)`-OR (begge nye
|
||
hjelpefunksjoner i `team_authz.py`). `user_is_tournament_participant` er
|
||
FLYTTET dit fra `registration.py` (het `is_participant` der) — org-scopede
|
||
routere kan ikke importere fra `registration.py` uten sirkulær import
|
||
(registration.py importerer FRA dem), men alle importerer allerede fritt
|
||
fra den avhengighetsfrie `team_authz.py`. `courses.py` sin `list_holes`
|
||
fikk en bevisst LØSERE sjekk (`user_is_org_player` — kun "koblet til NOEN
|
||
spillerprofil i org-en", ikke bundet til én turnering) siden par/
|
||
stroke-index er lavsensitiv banedata, ikke spillerdata.
|
||
|
||
**`/auth/me` utvidet** med `my_session_id`/`my_match_id` per rad i
|
||
`my_tournaments` (en av spillerens egne matcher, ikke-avgjort foretrukket)
|
||
— lar frontend lenke direkte til riktig chat/scorekort uten at spilleren
|
||
selv må navigere via program-/blind draw-skjermene (som fortsatt krever
|
||
org-medlemskap for andre formål). "Mine runder"-kortet i `dashboard.tsx`
|
||
fikk to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når
|
||
`my_match_id` finnes).
|
||
|
||
**Scratch-verifisert grundig, 43 automatiserte sjekker** (isolert
|
||
`teecup_app_scratch`-rolle, isolert scratch-MinIO, engangs API-container):
|
||
en rostret spiller UTEN org-medlemskap fikk korrekt tilgang til alt de ni
|
||
endepunktene dekker (inkl. faktisk å SENDE en chat-melding og et
|
||
hull-resultat); samme spiller fortsatt korrekt AVVIST fra det andre laget
|
||
sin chat; en helt fremmed innlogget bruker (ingen spillerkobling i org-en
|
||
i det hele tatt) avvist overalt; org-eier beholder full tilgang til alt
|
||
UNNTATT lag-chat (ekte privat, med vilje uendret — ADR-025); kryss-org-
|
||
isolasjon bekreftet (kan ikke nå egen turnering via en ANNEN org-id);
|
||
og — et reelt funn UNDER testingen, ikke en bug — en rostret-men-ikke-
|
||
utpekt-kaptein spiller ble først FEILAKTIG godtatt til walkover fordi
|
||
laget ennå ikke hadde noen utpekt kaptein (allerede dokumentert,
|
||
tiltenkt fallback i `user_is_team_captain`: "ingen kaptein ennå = enhver
|
||
rostret spiller godtas") — testen ble korrigert (la til en faktisk
|
||
kaptein) og bekreftet deretter riktig avvisning av ikke-kapteinen.
|
||
`test_isolation.sql` fortsatt 12/12 (ingen skjemaendring). Ekte
|
||
typesjekket produksjonsbuild av frontend kjørt og bekreftet.
|
||
|
||
**Rullet ut live 2026-07-21**, bruker bekreftet eksplisitt: ingen
|
||
migrasjon, `docker compose up -d --build teecup_api teecup_frontend`,
|
||
begge containere boot-et rent, `/health`/`/dashboard` → 200, `teeoff.no`
|
||
upåvirket.
|
||
|
||
---
|
||
|
||
## Dashboard: tom-tilstand ved første innlogging — 📋 UNDER REVURDERING (2026-07-21), IKKE bygget
|
||
|
||
Brukeren påpekte 2026-07-20 at dagens tomme-tilstand ("Du har ingen
|
||
organisasjon ennå — opprett en") er organisator-vridd og ikke stemmer med
|
||
hva en fersk bruker faktisk trenger å se/gjøre. Et opprinnelig forslag
|
||
(utvid "Mine runder" til påmeldinger + nøytral to-valgs tom-skjerm, se
|
||
historikk under) ble lagt frem 2026-07-20 — brukeren ba 2026-07-21
|
||
eksplisitt om å justere retningen i lys av en dypere refleksjon, se under.
|
||
|
||
**2026-07-21 — premisset er endret, ikke bare forslaget:** brukeren stilte
|
||
selv spørsmålet om organisasjon fortsatt bør være "det som meldes først" —
|
||
gitt ADR-031 (Mine runder), ADR-032 (e-post/mobil som personlig identitet)
|
||
og det nye ønsket om frittstående rundeføring med statistikk (se egen
|
||
seksjon rett under), er en vanlig bruker først og fremst en GOLFSPILLER,
|
||
og det å arrangere turneringer er én av flere ting en spiller *kan* gjøre —
|
||
ikke forutsetningen for å bruke appen i det hele tatt. **Vurdering: ja,
|
||
organisasjon bør slutte å være default/første-handling**, og bli ett
|
||
likestilt valg blant flere fremtidige "første ting du kan gjøre"
|
||
(bli med i en turnering via kode, registrere en runde selv, ELLER
|
||
arrangere/opprette organisasjon) — ikke lenger den ENESTE synlige veien
|
||
inn.
|
||
|
||
**Konsekvens for byggerekkefølgen:** selve tom-skjerm-redesignet er satt
|
||
PÅ VENT til frittstående runder (under) er avklart nok til å vite hvilken
|
||
tredje kortform den skal ha på tom-skjermen — å bygge en to-valgs versjon
|
||
nå og redesigne den på nytt om kort tid ville vært dobbeltarbeid. Punktet
|
||
"utvid Mine runder til rene påmeldinger" (fra 2026-07-20-forslaget)
|
||
henger IKKE sammen med denne avhengigheten og kan bygges uavhengig når som
|
||
helst — fortsatt et åpent, godt avgrenset TODO.
|
||
|
||
**Opprinnelig forslag (2026-07-20), for historikkens skyld:**
|
||
1. Utvid "Mine runder" til også å vise rene påmeldinger (ikke bare
|
||
rostrede lag).
|
||
2. Gjør selve tom-skjermen nøytral: to likestilte valg side ved side —
|
||
"Har du en kode?" og "Skal du arrangere selv? Opprett organisasjon".
|
||
|
||
**Venter på:** en videre avklaring av frittstående runder (under) før
|
||
tom-skjermens endelige form kan bestemmes.
|
||
|
||
---
|
||
|
||
## Frittstående rundeføring + detaljert statistikk (uten turnering/organisasjon) — 📋 NOTERT 2026-07-21, IKKE designet/bygget
|
||
|
||
Reist av brukeren 2026-07-21, eksplisitt begrunnet som relevant for
|
||
hvordan dashbordet skal se ut fremover (se punktet over) — derfor fanget
|
||
grundig her selv om ingenting bygges i denne runden.
|
||
|
||
**Brukerens beskrevne behov, fanget presist:** en bruker skal kunne
|
||
registrere en golfrunde HELT UAVHENGIG av enhver turnering eller
|
||
organisasjon — verken tilhørighet til et lag, en turnering, eller en
|
||
organisasjon skal være en forutsetning. Kan føres kun for seg selv, ELLER
|
||
for andre man spiller sammen med i flighten (ikke nødvendigvis
|
||
TeeCup-brukere). Statistikk utover selve slagtallet:
|
||
- Antall slag på hullet (allerede dekket av eksisterende `hole_score`-form)
|
||
- Antall putter
|
||
- Antall chip
|
||
- Antall bunkerslag
|
||
- Antall straffeslag
|
||
- Lengde på første putt
|
||
- "og lignende" — trolig flere finkornede stats brukeren vil spesifisere
|
||
nærmere når dette faktisk designes
|
||
|
||
**Dette er IKKE en liten dashboard-finpuss — det utfordrer en av
|
||
arkitektur-invariantene i CLAUDE.md direkte:** "Tenant = organisasjon.
|
||
`organization_id` på alle domenetabeller, håndhevet av RLS." En
|
||
frittstående runde har per definisjon INGEN organisasjon å henge
|
||
`organization_id` på — dagens RLS-modell (org_isolation-policyer som
|
||
alle stoler på `app.current_org`) dekker rett og slett ikke dette
|
||
tilfellet. Dette krever en ny, egen beslutning (sannsynligvis en helt ny
|
||
ADR) om et PARALLELT eierskaps-/isolasjonsmønster keyet på `user_id`
|
||
(`app_user.id`) i stedet for `organization_id` — ikke en utvidelse av et
|
||
eksisterende mønster, men en ny gren i tenant-modellen. Presist hvilke
|
||
tabeller som trengs (egen `personal_round`? egen
|
||
`personal_round_hole_stat`? gjenbruk av eksisterende `hole_score`-form med
|
||
en nullable `organization_id` og en NY RLS-policy for "eier = current
|
||
user"?) er IKKE avklart — bevisst ikke gjettet på her.
|
||
|
||
**Andre åpne spørsmål som trengs FØR design/bygging, ikke besvart av
|
||
brukerens beskrivelse ennå:**
|
||
- Hvordan identifiseres "andre man spiller med i flighten" når de ikke
|
||
nødvendigvis er TeeCup-brukere — frittstående "midlertidige" spiller-
|
||
rader (ala `player`, men uten organisasjonstilhørighet), eller rene
|
||
navn uten noen kobling i det hele tatt?
|
||
- Skal disse rundene noensinne telle inn i HCP-beregning/-historikk (se
|
||
eget punkt under), eller er de rent loggførende (som en digital
|
||
scorekort-dagbok)?
|
||
- Skal banedata (hull/par/stroke index/tee-rating) hentes fra samme
|
||
`course`-modell som i dag (org-scopet), eller trengs en egen,
|
||
org-uavhengig banekatalog for dette bruksmønsteret (en spiller uten
|
||
noen organisasjon i det hele tatt må fortsatt kunne velge en bane)?
|
||
- Skal frittstående runder vises i "Mine runder" på dashbordet sammen med
|
||
turnering-rundene, eller i en egen seksjon?
|
||
|
||
**Bevisst IKKE startet i denne runden** — dette bør bli sin egen,
|
||
dedikerte ADR-runde (arkitektur-invariant-nivå beslutning, ikke et
|
||
tillegg til en dashboard/konto-poleringsrunde), men er tatt med i
|
||
vurderingen av tom-tilstand-redesignet over siden det direkte påvirker
|
||
hvilke "første handling"-alternativer dashbordet bør vise i fremtiden.
|
||
|
||
---
|
||
|
||
## Én person, flere e-postadresser — DEL 1 (det enkle tilfellet) ✅ BYGGET OG LIVE 2026-07-21, DEL 2 (kontosammenslåing) fortsatt 📋 NOTERT
|
||
|
||
Reist av brukeren rett etter ADR-032 (verifisert e-postbytte). Et beslektet,
|
||
men DISTINKT behov: én person kan ha flere e-postadresser i omløp samtidig
|
||
(f.eks. registrert seg privat med én adresse, men fått en turnering-
|
||
invitasjon rettet mot en jobb-adresse organisator la inn) — ikke et BYTTE
|
||
(ADR-032 sin løsning), men en TILLEGGS-tilknytning. I dag oppretter et
|
||
magic-link-innlogg på en ny adresse alltid en HELT NY, tom `app_user`-konto
|
||
(ADR-009) — nøyaktig det som gjør at spilleren aldri "finner" turneringen
|
||
sin med sin vanlige, primære konto.
|
||
|
||
**Brukerens beskrevne flyt, ordrett fanget:** innlogget med
|
||
eksempel@domene.no, sier "jeg eier også test@domain.com". Ved dette kravet
|
||
sendes en e-post til test@domain.com med en nøkkel som limes inn et sted på
|
||
dashbordet. Etter bekreftelse dukker inviterte turneringer på den adressen
|
||
opp. Annen data (spilte runder, personlig informasjon) skal "forespørres
|
||
slått sammen eller justert". Fremtidige innlogginger skal kunne gjøres med
|
||
ENHVER av de tilknyttede adressene.
|
||
|
||
**Foreslått retning, basert på gjenbruk av allerede bygget mønster:**
|
||
samme token-i-e-post-bevis-eierskap-mekanisme som ADR-032 sin
|
||
e-postbytte-flyt (`email_change_token`), men ADDITIV i stedet for
|
||
ERSTATTENDE — en ny tabell for verifiserte SEKUNDÆRE e-poster knyttet til
|
||
kontoen (i stedet for å overskrive `app_user.email`). Login (`/auth/
|
||
request-link` m.fl.) må da slå opp BÅDE primær- og sekundær-e-poster.
|
||
`link_player_by_email()`/organisasjonsinvitasjon-aksept (som i dag kun
|
||
kjører mot `app_user.email`) må kjøres for HVER av kontoens verifiserte
|
||
adresser — naturlig utløst rett etter en ny adresse er bekreftet, og
|
||
sannsynligvis også trygt å kjøre på nytt ved hver innlogging (idempotent,
|
||
samme mønster som i dag).
|
||
|
||
**Det virkelig vanskelige, uløste spørsmålet, IKKE adressert av brukerens
|
||
beskrevne flyt:** hva skjer hvis den "krevde" adressen ALLEREDE er
|
||
primær- (eller sekundær-)adressen til en ANNEN, eksisterende `app_user`-
|
||
konto — altså at spilleren faktisk har logget inn med DEN adressen
|
||
tidligere og dermed har to helt separate kontoer med egen historikk
|
||
(ulike org-medlemskap, ulike spillerkoblinger, kanskje ulikt passord/2FA)?
|
||
Da holder det ikke å bare "legge til" adressen — det er en ekte KONTO-
|
||
SAMMENSLÅING (slå sammen organisasjonsmedlemskap uten å bryte "én rolle
|
||
per bruker per org"-unikheten, deduplisere spiller-koblinger, avgjøre
|
||
hvilken konto som "vinner" for tvetydige felt som `preferred_locale`/2FA
|
||
når begge har satt noe ulikt). Dette er en betydelig større og mer
|
||
risikofylt operasjon enn "legg til en frisk, ukrevd adresse" — bør
|
||
utredes og besluttes som en egen, separat sak, ikke antas løst av samme
|
||
runde som det enkle tilfellet.
|
||
|
||
**Plassering:** brukeren presiserte eksplisitt at dette må skje FRA
|
||
dashbord-siden (ikke `/account`, der ADR-032 sin e-postbytte-flyt ellers
|
||
naturlig ville hørt hjemme) — trolig fordi selve GEVINSTEN (nye turneringer
|
||
dukker opp) er noe som vises på dashbordet, så handlingen bør ligge der
|
||
resultatet vises.
|
||
|
||
**Ble delt i to separate runder, som foreslått:** (1) legg til en frisk,
|
||
ukrevd sekundær-e-post — ✅ BYGGET, se under. (2) Ekte konto-sammenslåing
|
||
for det vanskelige tilfellet — fortsatt IKKE designet, egen fremtidig
|
||
runde.
|
||
|
||
### Del 1 (det enkle tilfellet) — ✅ BYGGET OG LIVE 2026-07-21
|
||
|
||
Migrasjon `017_secondary_email.sql`: to nye tabeller
|
||
(`secondary_email_token` — midlertidig, samme token-hash-og-utløp-mønster
|
||
som `email_change_token`; `user_secondary_email` — den faktiske,
|
||
verifiserte adressen, globalt UNIQUE). Nye endepunkter i
|
||
`app/routers/auth.py`: `POST /auth/secondary-email` (send
|
||
bekreftelseslenke), `POST /auth/secondary-email/confirm` (ingen sesjon
|
||
påkrevd, samme mønster som selve magic-link-verifiseringen),
|
||
`DELETE /auth/secondary-email/{id}`.
|
||
|
||
**Kjernestykket, ikke bare CRUD:** `verify_magic_link` og
|
||
`login_with_password` sjekker nå `user_secondary_email` FØR de gjør sitt
|
||
vanlige `app_user.email`-oppslag — finner de en match, løses innloggingen
|
||
til DEN EKSISTERENDE eierens konto i stedet for å (som før) stille
|
||
opprette en helt ny, separat konto. Dette er selve mekanismen som gjør
|
||
adressen nyttig, ikke bare en liste over "andre adresser".
|
||
|
||
**Plassering, bevisst avvik fra brukerens opprinnelige "fra dashbordet"-
|
||
instruks:** lagt i `/account` (samme sted som ADR-032 sin e-postbytte),
|
||
IKKE dashbordet — begrunnet med at dette kun er del 1 (det enkle
|
||
tilfellet); når/hvis del 2 (kontosammenslåing, "data dukker opp") bygges,
|
||
er dashbordet trolig riktigere siden GEVINSTEN vises der. Flagget
|
||
eksplisitt til bruker, ikke stille besluttet.
|
||
|
||
**Scratch-verifisert, 20 sjekker:** adresse legges IKKE til før bekreftet;
|
||
token ikke gjenbrukbart; adresse som allerede er en ANNEN kontos
|
||
hovedadresse ELLER sekundæradresse avvist tydelig (409 DUPLICATE) i begge
|
||
retninger; innlogging via sekundæradressen (BÅDE magic-link OG passord)
|
||
løses korrekt til samme, eksisterende konto (bekreftet: samme `id`,
|
||
`email` i responsen forblir hovedadressen); en fremmed kan ikke slette
|
||
andres sekundæradresse; og — den kritiske sjekken — en ny innlogging på
|
||
adressen ETTER at den er fjernet oppretter en genuint NY, separat konto
|
||
(beviser fjerningen er reell, ikke kosmetisk). `test_isolation.sql`
|
||
fortsatt 12/12 (additiv migrasjon). Ekte typesjekket produksjonsbuild av
|
||
frontend kjørt og bekreftet.
|
||
|
||
**Rullet ut live 2026-07-21**, bruker bekreftet eksplisitt: migrasjon 017
|
||
kjørt mot ekte `teecup_db` (bekreftet begge nye tabeller finnes,
|
||
`test_isolation.sql` fortsatt 12/12), deretter `docker compose up -d
|
||
--build teecup_api teecup_frontend`. Begge containere boot-et rent,
|
||
`/health`/`/dashboard`/`/account`/`/verify-email` → 200, `teeoff.no`
|
||
upåvirket.
|
||
|
||
### Del 2 (ekte kontosammenslåing) — fortsatt 📋 NOTERT, IKKE designet
|
||
|
||
Uendret fra den opprinnelige analysen: hva skjer hvis adressen som legges
|
||
til ALLEREDE er primær- eller sekundæradressen til en ANNEN, eksisterende
|
||
konto (spilleren har altså to helt separate kontoer med egen historikk —
|
||
ulike org-medlemskap, ulike spillerkoblinger, kanskje ulikt passord/2FA)?
|
||
Dagens del 1-løsning avviser dette tydelig (409 DUPLICATE) i stedet for å
|
||
gjette — en ekte sammenslåing (slå sammen org-medlemskap uten å bryte
|
||
"én rolle per bruker per org", deduplisere spillerkoblinger, avgjøre
|
||
hvilken konto som "vinner" for motstridende felt) er en betydelig større
|
||
og mer risikofylt operasjon, fortsatt bevisst utsatt til en egen,
|
||
dedikert designrunde.
|
||
|
||
---
|
||
|
||
## 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).
|