2026-07-16 07:18:01 +02:00
# 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)
>
2026-07-17 21:40:42 +02:00
> Sist oppdatert: 2026-07-17
2026-07-16 07:18:01 +02:00
---
## 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. |
2026-07-16 14:03:50 +02:00
| Brutto/netto + prosent-allowance (75 %, 90 %, 3/4) | ✅ | ADR-005. Prosentene bekreftet mot Golf GameBook (ADR-014). |
2026-07-16 07:18:01 +02:00
| 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. |
2026-07-16 14:38:42 +02:00
| 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. |
2026-07-18 15:54:40 +02:00
| 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. |
2026-07-16 14:38:42 +02:00
| Konfigurerbar handicap-pipeline (4 brytere) | ✅ | ADR-014. Bygget i `app/handicap.py` , brukt av scoring-runden. |
2026-07-16 20:59:59 +02:00
| 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-buggen er fikset og verifisert grundig.
Fiksen: ny migrasjon 005_rls_null_guard.sql — én delt STABLE SQL-funksjon app_current_org() (NULLIF(current_setting('app.current_org', true), '')::uuid) erstatter det rå uttrykket i alle 15 RLS-policyer (14 org_isolation + org_self) via ALTER POLICY. NULLIF konverterer tomstreng til NULL før cast, så "trygg standard: se ingenting" gjenopprettes uansett GUC-tilstand.
Verifisert to ganger, ulikt:
Tre nye regresjonstester i test_isolation.sql (Test 10–12): tomstreng-lesing gir 0 rader ikke krasj, tomstreng-skriving avvises av RLS ikke krasj, og org-bootstrap-innsetting (tidligere aldri testet) fungerer rett etter tomstreng-tilstand.
Faktisk gjenskaping av original-buggen mot en ekte container (pool-størrelse tvunget til 1 for å garantere tilkoblings-gjenbruk): varmet opp med et vanlig org_connection()-kall, kalte deretter /auth/me på samme tilkobling — gikk fra 500 til 200.
En viktig ting jeg tok feil om i forrige runde, oppdaget ved å faktisk teste i stedet for å anta: fiksen gjør ikke at /auth/me trygt kan joine organization direkte via plain_connection(). Den løser tomstreng-krasjen, men org_self-policyen krever fortsatt en matchende app.current_org for å vise noen rad i det hele tatt — riktig RLS-design, ikke noe fiksen skulle endre. Siden en bruker kan tilhøre flere organisasjoner, finnes det ingen én kontekst å sette. Rettet til riktig løsning: /auth/me slår nå opp hvert org-navn enkeltvis via org_connection() (verifisert at det faktisk returnerer navnet korrekt).
Alt dokumentert i CLAUDE.md/FEATURE_BACKLOG.md.
2026-07-16 15:34:24 +02:00
| RLS-tomstreng-fiks (`app_current_org()`) | ✅ | Migrasjon `005_rls_null_guard.sql` . Se detaljer under. |
2026-07-16 20:41:34 +02:00
| Organisasjon-bootstrap (opprette ny org via API) | ✅ | `POST /orgs` , `app/routers/organizations.py` . Se detaljer under. |
2026-07-16 20:59:59 +02:00
| Ekte SMTP-utsending av magic-link | ✅ | `app/email.py` . Se detaljer under. |
TeeCup er nå containerisert og live på https://teecup.teeoff.no. Dette var den mest hendelsesrike runden denne økten — første gang noe rørte ekte, permanent infrastruktur, og det viste seg berettiget:
To reelle driftshendelser, begge funnet og rettet i sanntid:
Caddy plukket ikke opp filendringen min — enkeltfil-bind-mount er låst til inoden fra da containeren sist startet; min redigering (atomisk rename) laget en ny inode, så validate/reload/admin-API opererte alle stille på den gamle filen. Løst med full omstart av teeoff_caddy (du bekreftet eksplisitt, siden det avvek fra planens "ingen omstart"-løfte).
Alvorlig nettverkskollisjon — oppdaget rett etterpå da ekte teeoff-trafikk (/api/facilities?... med ekte klubbnavn) dukket opp i teecup_api sin logg. docker-compose.yml sin service-nøkkel api: kolliderte med teeoffs eget api-servicenavn på det delte nettverket — Docker Compose gir service-navn som DNS-alias, så begge containerne delte alias api, og Caddy kunne tilfeldig sende ekte brukertrafikk til TeeCup i stedet. Stoppet teecup_api umiddelbart, ga den navnet teecup_api i stedet, bekreftet kollisjonen er borte.
Mindre ting underveis: jeg eksponerte ved et uhell to secret-verdier i eget debug-output (du roterte passordet), og lærte at Docker ikke leser .env på nytt for en kjørende container (krever --force-recreate etter hver endring) og at # i et upassordet passord kuttes som kommentar av Compose.
Sluttresultat, verifisert ende-til-ende mot den ekte, live stacken: teeoff.no upåvirket gjennom hele prosessen, teecup.teeoff.no/health → 200 med automatisk TLS, full magic-link-innlogging med ekte e-postlevering og Secure-cookie.
CLAUDE.md/FEATURE_BACKLOG.md oppdatert med full detaljer, inkludert en generell lærdom for fremtidige tjenester på delt nettverk. To repoer har uncommittede endringer: /opt/teecup (nye Dockerfile/docker-compose.yml/.dockerignore + statusfiler) og /opt/teeoff (Caddyfile-endringen, egen repo). Ingenting committet ennå — si ifra når du vil det.
2026-07-16 21:47:34 +02:00
| Containerisert, LIVE på `teecup.teeoff.no` | ✅ | `Dockerfile` + `docker-compose.yml` . Se egen seksjon under — to reelle driftshendelser funnet og rettet. |
2026-07-17 21:40:42 +02:00
| 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. |
Alt rent, ingen byggeartefakter liggende igjen, dokumentasjonen oppdatert. Oppsummering:
Live og verifisert ende-til-ende:
/verify-siden bygget og live, e-postmalen sender nå en ekte klikkbar lenke (app/email.py/app/config.py)
Frontend containerisert (frontend/Dockerfile, standalone Next.js) og rullet ut som egen teecup_frontend-tjeneste
Caddy peker nå teecup.teeoff.no på frontend-en, som selv proxyer API-kall server-side — same-origin, ingen CORS, cookie uendret (dokumentert som ny ADR-016)
Fant og fikset en reell fallgruve: Next.js sin rewrites() bakes inn ved build-tid for standalone-output, ikke lest ved kjøretid — løst med en Docker build-time ARG
Ekte e-post sendt, ekte lenke klikket, sesjon opprettet — bekreftet av deg
Én ting du må huske: Caddy-endringen ligger i det separate /opt/teeoff-repoet (deploy/Caddyfile), ikke i teecup-repoet — den er ikke committet ennå. Lett å glemme siden resten av denne økten kun har jobbet i /opt/teecup. Vil du at jeg minner deg, eller committer du den nå selv i teeoff-repoet?
CLAUDE.md, FEATURE_BACKLOG.md og ARCHITECTURE_DECISIONS.md (ny ADR-016) er oppdatert. Klar for commit i teecup-repoet når du vil.
2026-07-17 22:28:43 +02:00
| 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. |
2026-07-18 06:53:42 +02:00
| 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). |
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
| Frontend: lag/roster-skjerm, LIVE | ✅ | `/tournaments/[id]` . To nye backend-endepunkter bygget samtidig (`PATCH`/`DELETE` roster). Skrive-flyt ikke testet med ekte data ennå. |
2026-07-18 09:03:37 +02:00
| 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). |
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
| Roster: endre kaptein / fjern spiller (`PATCH`/`DELETE`) | ✅ | `app/routers/tournaments.py` . Bevisst ingen "kun én kaptein"-håndhevelse ennå — se «Brukerroller»-punktet under. |
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
| 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. |
Alt rent, ingen byggeartefakter liggende igjen, dokumentasjonen oppdatert. Oppsummering:
Live og verifisert ende-til-ende:
/verify-siden bygget og live, e-postmalen sender nå en ekte klikkbar lenke (app/email.py/app/config.py)
Frontend containerisert (frontend/Dockerfile, standalone Next.js) og rullet ut som egen teecup_frontend-tjeneste
Caddy peker nå teecup.teeoff.no på frontend-en, som selv proxyer API-kall server-side — same-origin, ingen CORS, cookie uendret (dokumentert som ny ADR-016)
Fant og fikset en reell fallgruve: Next.js sin rewrites() bakes inn ved build-tid for standalone-output, ikke lest ved kjøretid — løst med en Docker build-time ARG
Ekte e-post sendt, ekte lenke klikket, sesjon opprettet — bekreftet av deg
Én ting du må huske: Caddy-endringen ligger i det separate /opt/teeoff-repoet (deploy/Caddyfile), ikke i teecup-repoet — den er ikke committet ennå. Lett å glemme siden resten av denne økten kun har jobbet i /opt/teecup. Vil du at jeg minner deg, eller committer du den nå selv i teeoff-repoet?
CLAUDE.md, FEATURE_BACKLOG.md og ARCHITECTURE_DECISIONS.md (ny ADR-016) er oppdatert. Klar for commit i teecup-repoet når du vil.
2026-07-17 22:28:43 +02:00
---
### 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.
2026-07-17 21:40:42 +02:00
---
### 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).
TeeCup er nå containerisert og live på https://teecup.teeoff.no. Dette var den mest hendelsesrike runden denne økten — første gang noe rørte ekte, permanent infrastruktur, og det viste seg berettiget:
To reelle driftshendelser, begge funnet og rettet i sanntid:
Caddy plukket ikke opp filendringen min — enkeltfil-bind-mount er låst til inoden fra da containeren sist startet; min redigering (atomisk rename) laget en ny inode, så validate/reload/admin-API opererte alle stille på den gamle filen. Løst med full omstart av teeoff_caddy (du bekreftet eksplisitt, siden det avvek fra planens "ingen omstart"-løfte).
Alvorlig nettverkskollisjon — oppdaget rett etterpå da ekte teeoff-trafikk (/api/facilities?... med ekte klubbnavn) dukket opp i teecup_api sin logg. docker-compose.yml sin service-nøkkel api: kolliderte med teeoffs eget api-servicenavn på det delte nettverket — Docker Compose gir service-navn som DNS-alias, så begge containerne delte alias api, og Caddy kunne tilfeldig sende ekte brukertrafikk til TeeCup i stedet. Stoppet teecup_api umiddelbart, ga den navnet teecup_api i stedet, bekreftet kollisjonen er borte.
Mindre ting underveis: jeg eksponerte ved et uhell to secret-verdier i eget debug-output (du roterte passordet), og lærte at Docker ikke leser .env på nytt for en kjørende container (krever --force-recreate etter hver endring) og at # i et upassordet passord kuttes som kommentar av Compose.
Sluttresultat, verifisert ende-til-ende mot den ekte, live stacken: teeoff.no upåvirket gjennom hele prosessen, teecup.teeoff.no/health → 200 med automatisk TLS, full magic-link-innlogging med ekte e-postlevering og Secure-cookie.
CLAUDE.md/FEATURE_BACKLOG.md oppdatert med full detaljer, inkludert en generell lærdom for fremtidige tjenester på delt nettverk. To repoer har uncommittede endringer: /opt/teecup (nye Dockerfile/docker-compose.yml/.dockerignore + statusfiler) og /opt/teeoff (Caddyfile-endringen, egen repo). Ingenting committet ennå — si ifra når du vil det.
2026-07-16 21:47:34 +02:00
---
### 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).
2026-07-16 20:59:59 +02:00
---
### 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.
2026-07-16 20:41:34 +02:00
---
### 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.
Ekte autentisering er bygget og verifisert. X-Debug-User-Id-stubben er helt fjernet, ingen fallback beholdt.
Ny flyt: magic-link (POST /auth/request-link → POST /auth/verify-link) + JWT-sesjon i HttpOnly/SameSite=Lax/dynamisk-Secure-cookie (30 dager), pluss /auth/logout og /auth/me. Ny migrasjon 004_auth.sql (unik e-post-indeks + magic_link_token-tabell).
Sikkerhetsdesignet fra Plan-agent-gjennomgangen holdt gjennom testing:
Token: secrets.token_urlsafe(32), kun SHA-256-hash lagres
Atomisk forbruk (UPDATE...RETURNING, ikke les-sjekk-skriv) — hindrer replay
Generisk respons uansett om e-posten finnes — hindrer enumerering
app_user opprettes først ved vellykket verifisering, ikke ved forespørsel — hindrer massopprettelse
Gamle uforbrukte lenker ugyldiggjøres når en ny utstedes
PyJWT (byttet fra python-jose pga. bredere sårbarhetsflate) med eksplisitt algorithms=["HS256"]
Ekte eksistens-sjekk mot app_user på hvert kall — en slettet bruker mister tilgang umiddelbart, ikke etter 30 dager
Alle 12 planlagte tester bestått, inkludert cooldown, token-ugyldiggjøring, utløp, tuklet JWT, slettet bruker, og at debug-headeren nå er helt uten effekt.
To ting funnet og fikset/dokumentert underveis:
ON CONFLICT (email) matchet ikke den nye partielle unike indeksen uten eksplisitt WHERE-klausul — fikset.
En reell, dypere RLS-bug (dokumentert i FEATURE_BACKLOG.md, ikke fikset her): organization-tabellens RLS-policy kaster en 500 i stedet for "se ingenting" når app.current_org leses tilbake som tomstreng (ikke NULL) på en gjenbrukt pool-tilkobling. Berører trolig alle 15 RLS-policyer i skjemaet — for stort og sensitivt (ADR-003-grunnmuren) til å hastefikse her, så jeg mitigerte det lokalt i /auth/me og satte det som punkt 1 i neste-steg-listen.
2026-07-16 15:16:53 +02:00
---
RLS-tomstreng-buggen er fikset og verifisert grundig.
Fiksen: ny migrasjon 005_rls_null_guard.sql — én delt STABLE SQL-funksjon app_current_org() (NULLIF(current_setting('app.current_org', true), '')::uuid) erstatter det rå uttrykket i alle 15 RLS-policyer (14 org_isolation + org_self) via ALTER POLICY. NULLIF konverterer tomstreng til NULL før cast, så "trygg standard: se ingenting" gjenopprettes uansett GUC-tilstand.
Verifisert to ganger, ulikt:
Tre nye regresjonstester i test_isolation.sql (Test 10–12): tomstreng-lesing gir 0 rader ikke krasj, tomstreng-skriving avvises av RLS ikke krasj, og org-bootstrap-innsetting (tidligere aldri testet) fungerer rett etter tomstreng-tilstand.
Faktisk gjenskaping av original-buggen mot en ekte container (pool-størrelse tvunget til 1 for å garantere tilkoblings-gjenbruk): varmet opp med et vanlig org_connection()-kall, kalte deretter /auth/me på samme tilkobling — gikk fra 500 til 200.
En viktig ting jeg tok feil om i forrige runde, oppdaget ved å faktisk teste i stedet for å anta: fiksen gjør ikke at /auth/me trygt kan joine organization direkte via plain_connection(). Den løser tomstreng-krasjen, men org_self-policyen krever fortsatt en matchende app.current_org for å vise noen rad i det hele tatt — riktig RLS-design, ikke noe fiksen skulle endre. Siden en bruker kan tilhøre flere organisasjoner, finnes det ingen én kontekst å sette. Rettet til riktig løsning: /auth/me slår nå opp hvert org-navn enkeltvis via org_connection() (verifisert at det faktisk returnerer navnet korrekt).
Alt dokumentert i CLAUDE.md/FEATURE_BACKLOG.md.
2026-07-16 15:34:24 +02:00
### 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.
2026-07-16 07:18:01 +02:00
---
## Ønsket, men IKKE fanget før nå (fra Gemini-samtalene)
2026-07-19 22:51:07 +02:00
### 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).
2026-07-16 07:18:01 +02:00
- 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 11:41:57 +02:00
- **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.
Update Todos
Refaktorere get_leaderboard til delt fetch_leaderboard()
Gjøre own_team_ids null-sikker for anonyme/uinnloggede tilskuere
Refaktorere list_matches og get_scorecard til delte funksjoner
Legge til offentlige leaderboard/matches/scorecard-endepunkter i registration.py
Skrive ADR-026 (tilskuer-rolle)
Scratch-verifisere backend
Bygge frontend: offentlig live-visning (leaderboard + matcher + scorekort) på /t/[id]/live
Ekte typesjekket frontend-build
Deploy etter bekreftelse + oppdatere .md-filer
Tilskuer-rollen er live. Kjernen: ingen ny rolle trengtes — «tilskuer» er ganske enkelt enhver som allerede kan se turneringen (per synlighetsnivå), nå utvidet til å dekke leaderboard, matchliste og fullt hull-for-hull-scorekort, ikke bare info-siden og programtider som før. Ny /t/[id]/live-side, lenket fra hovedsiden.
Fant og lukket to sikkerhetshull under design (før noe ble bygget): en manglende sjekk på at økt/match i URL-en faktisk hører til turneringen i samme URL (ellers kunne noen lest en hvilken som helst økt i samme organisasjon ved å gjette id-er), og en manglende reveal-sjekk på scorekortet. Begge verifisert eksplisitt med egne tester — 16 automatiserte sjekker totalt, inkludert en presis test med to ekte turneringer i samme organisasjon.
.md-filene er oppdatert. Gjenværende åpne punkter: PWA, kode-regenerering, korrigering-godkjenning, sanntid for leaderboard-siden (WebSocket-mekanismen finnes allerede, bare ikke koblet til der ennå), og de nye turneringsformatene som ble notert tidligere.
2026-07-19 22:53:33 +02:00
- **Tilskuer — ✅ HELT FERDIG, LIVE 2026-07-19 (ADR-026),** rett etter
2026-07-19 22:51:07 +02:00
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).
2026-07-16 07:18:01 +02:00
2026-07-16 14:38:42 +02:00
### Scoring-autorisasjon: hvem fører, hvem korrigerer, hvem lukker (rejst 2026-07-16)
2026-07-19 11:41:57 +02:00
- **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.
2026-07-16 14:38:42 +02:00
- **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).
2026-07-16 14:49:42 +02:00
- **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).
2026-07-19 21:51:05 +02:00
Turnering-status kan nå settes via API — se eget punkt lenger ned.
2026-07-19 21:36:33 +02:00
- **Walkover/konsesjon — ✅ BYGGET OG LIVE 2026-07-19 (ADR-024).** Løste det
2026-07-19 21:25:31 +02:00
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).
2026-07-19 21:51:05 +02:00
- **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.
2026-07-16 14:38:42 +02:00
- **Avgjort 2026-07-16:**
2026-07-16 14:49:42 +02:00
- **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.
2026-07-19 11:41:57 +02:00
- **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
2026-07-19 21:51:05 +02:00
for v1? (c) ~~skal turnering-status kunne settes via API~~ ✅ avgjort/
bygget 2026-07-19 (se over).
2026-07-16 14:38:42 +02:00
2026-07-16 07:18:01 +02:00
### Blind draw (skjult lagoppstilling)
2026-07-16 14:38:42 +02:00
- **Status:** ✅ skjema (migrasjon 003, `lineup_lock` ) + API bygget og verifisert
(`app/routers/matches.py`: synlighetsfilter i SQL, ikke Python-filter — se ADR-013).
2026-07-18 17:15:50 +02:00
**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.
2026-07-16 07:18:01 +02:00
- Kapteinene låser oppstillingen skjult; matchene avsløres samtidig når begge er
ferdige.
2026-07-19 11:41:57 +02:00
- **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å.
2026-07-18 17:15:50 +02:00
- **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.
2026-07-16 07:18:01 +02:00
### Forenklet scoreføring (uten slagtall)
2026-07-16 14:38:42 +02:00
- **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` ).
2026-07-18 17:47:33 +02:00
- **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.
2026-07-16 07:18:01 +02:00
### 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).
2026-07-18 18:22:10 +02:00
### 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.
2026-07-16 07:18:01 +02:00
### Push-varsler
- **Status:** 📋 planlagt (infrastruktur)
- «BREAKING: X vant matchen». PWA push. Egen infrastruktur-bit.
### Sanntid (WebSockets)
2026-07-19 23:14:17 +02:00
- **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.
2026-07-16 07:18:01 +02:00
2026-07-16 07:26:04 +02:00
### 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.
2026-07-19 22:26:45 +02:00
### 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.
2026-07-16 07:18:01 +02:00
---
Kommunikasjon er live — det største enkeltløftet i prosjektet så langt:
Lag-chat («det hemmelige rommet»), tilgjengelig via en ny chat-knapp på hvert lagkort i lag/spillere-skjermen — ekte privat, org-eier/admin har ikke tilgang (verifisert helt ned til selve WebSocket-håndtrykket, ikke bare REST).
Offentlig runde-feed («Banter Board»), ny seksjon nederst på turneringens offentlige side — gjenbruker eksisterende synlighetsnivåer, men posting krever innlogging + tilknytning til turneringen/organisasjonen.
Bilder i begge, sanntid via WebSockets i begge.
Underveis: fant at Next.js ikke proxyer WebSocket-oppgraderinger pålitelig, løst med en egen Caddy-rute rett til API-et (samme mønster som media-ruten fra MinIO-runden). Alt scratch-verifisert grundig (20 automatiserte sjekker inkl. faktisk sanntidsmottak over en åpen WebSocket, ikke bare REST-svar), og bekreftet på ekte produksjon med et reelt wss://-håndtrykk over https.
Viktig å huske til neste økt: Caddy-endringen ligger uncommitted i det separate /opt/teeoff-repoet — samme fallgruve som tidligere Caddy-runder.
Alt er dokumentert i .md-filene. Naturlig neste kandidat er tilskuer-rollen (som Kommunikasjon-arbeidet nå gjør mulig å definere skikkelig), men si fra hva du vil prioritere.
2026-07-19 22:33:13 +02:00
## Kommunikasjon — ✅ HELT FERDIG 2026-07-19 (ADR-025)
2026-07-16 07:18:01 +02:00
| Del | Status | Notat |
|---|---|---|
Kommunikasjon er live — det største enkeltløftet i prosjektet så langt:
Lag-chat («det hemmelige rommet»), tilgjengelig via en ny chat-knapp på hvert lagkort i lag/spillere-skjermen — ekte privat, org-eier/admin har ikke tilgang (verifisert helt ned til selve WebSocket-håndtrykket, ikke bare REST).
Offentlig runde-feed («Banter Board»), ny seksjon nederst på turneringens offentlige side — gjenbruker eksisterende synlighetsnivåer, men posting krever innlogging + tilknytning til turneringen/organisasjonen.
Bilder i begge, sanntid via WebSockets i begge.
Underveis: fant at Next.js ikke proxyer WebSocket-oppgraderinger pålitelig, løst med en egen Caddy-rute rett til API-et (samme mønster som media-ruten fra MinIO-runden). Alt scratch-verifisert grundig (20 automatiserte sjekker inkl. faktisk sanntidsmottak over en åpen WebSocket, ikke bare REST-svar), og bekreftet på ekte produksjon med et reelt wss://-håndtrykk over https.
Viktig å huske til neste økt: Caddy-endringen ligger uncommitted i det separate /opt/teeoff-repoet — samme fallgruve som tidligere Caddy-runder.
Alt er dokumentert i .md-filene. Naturlig neste kandidat er tilskuer-rollen (som Kommunikasjon-arbeidet nå gjør mulig å definere skikkelig), men si fra hva du vil prioritere.
2026-07-19 22:33:13 +02:00
| 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).
2026-07-16 07:18:01 +02:00
---
2026-07-18 10:58:36 +02:00
## Landingssider (turnering + organisasjon) — ADR-018 ✅ HELT FERDIG 2026-07-18
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
Reist av brukeren 2026-07-18, rett etter registrerings-ADR-en (ADR-017).
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
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.
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
| Del | Status | Notat |
|---|---|---|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
| 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. |
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
| 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. |
2026-07-18 10:58:36 +02:00
| 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). |
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
| 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}` . |
2026-07-18 10:32:53 +02:00
| Frontend: turnering-landingsside + påmeldingsskjema, LIVE | ✅ | `/t/[id]` . Tre bekreftelsestilstander (bekreftet/venteliste/godkjenning venter). Verifisert med ekte `POST` -registrering mot scratch. |
Klubb-landingssiden er live på https://teecup.teeoff.no/clubs/{slug} — dermed er ADR-018 sine to planlagte frontend-skjermer komplette.
Én reell komponentkonflikt løst, ikke duplisert bort: TournamentCard var bygget kun for den innloggede dashbord-konteksten. I stedet for å lage en egen kopi for den offentlige klubbsiden, gjorde jeg orgId valgfri — satt gir innlogget lenke, utelatt gir /t/{id} i stedet. Samme kort, to kontekster. Bekreftet dashbordet fortsatt fungerer uendret.
V0 laget selv en rute denne gangen, men kalte parameteren [id] selv om det faktisk er en slug — skrev en egen, riktig navngitt [slug]-rute i stedet.
Verifisert mot ekte scratch-data gjennom en kjørende frontend-dev-server: en org med to turneringer (én offentlig, én org-privat) — klubbsiden viste kun den offentlige, akkurat som filteret i API-et tilsier.
Live, teeoff.no upåvirket. Gjenstår av ADR-018: Open Graph-metadata for delingsforhåndsvisning, og MinIO/bilder som egen runde. Vil du ta Open Graph-metadataen nå, siden det er en liten, avgrenset bit?
2026-07-18 10:48:20 +02:00
| Frontend: org-/klubb-landingsside, LIVE | ✅ | `/clubs/[slug]` . Gjenbruker `TournamentCard` (nå med valgfri `orgId` ) for turneringslisten. Verifisert: viser kun `public` -synlige turneringer. |
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
---
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
### 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.
2026-07-18 12:29:24 +02:00
**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.
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
2026-07-19 08:17:12 +02:00
**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
2026-07-18 22:21:22 +02:00
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
2026-07-19 08:17:12 +02:00
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.
2026-07-18 22:21:22 +02:00
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
---
2026-07-18 15:54:40 +02:00
### 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.
2026-07-18 22:21:22 +02:00
**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.
2026-07-18 15:54:40 +02:00
---
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
## 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.
---
2026-07-19 10:05:42 +02:00
## 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. |
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
ADR-021 (passord/2FA) og ADR-022 (org-eierskap) er live. Kort oppsummert:
Nytt i innloggingen:
Passord (valgfritt tillegg til magic-link) — Argon2id, testet med ekte spesialtegn/mellomrom/æøå
2FA — TOTP eller e-post-engangskode, brukerens eget valg
Påkrevd 2FA for org-eiere/administratorer, valgfritt for andre
Ny /account-skjerm for å sette passord og styre 2FA
Nytt for organisasjoner:
Inviter andre på e-post til enhver rolle (eiere) eller kun medlem (administratorer)
Frasi deg eierskap selv, eller fjern andre medlemmer — med vern mot at en organisasjon står igjen uten eier
Superadmin-flagg (kun manuelt satt i databasen, aldri via API) som kan sette eierskap på hvilken som helst organisasjon
Ny /orgs/[id]/members-skjerm, lenket fra dashbordet
Fant og fikset tre reelle bugs underveis i scratch-testingen (ingen nådde produksjon) — en krasj i selve innloggingen og to tilfeller av en uendelig 2FA-løkke. Alt er nå verifisert grundig og rullet ut, eksisterende sesjoner er upåvirket.
2026-07-19 10:40:15 +02:00
| 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` . |
2026-07-19 10:05:42 +02:00
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
ADR-021 (passord/2FA) og ADR-022 (org-eierskap) er live. Kort oppsummert:
Nytt i innloggingen:
Passord (valgfritt tillegg til magic-link) — Argon2id, testet med ekte spesialtegn/mellomrom/æøå
2FA — TOTP eller e-post-engangskode, brukerens eget valg
Påkrevd 2FA for org-eiere/administratorer, valgfritt for andre
Ny /account-skjerm for å sette passord og styre 2FA
Nytt for organisasjoner:
Inviter andre på e-post til enhver rolle (eiere) eller kun medlem (administratorer)
Frasi deg eierskap selv, eller fjern andre medlemmer — med vern mot at en organisasjon står igjen uten eier
Superadmin-flagg (kun manuelt satt i databasen, aldri via API) som kan sette eierskap på hvilken som helst organisasjon
Ny /orgs/[id]/members-skjerm, lenket fra dashbordet
Fant og fikset tre reelle bugs underveis i scratch-testingen (ingen nådde produksjon) — en krasj i selve innloggingen og to tilfeller av en uendelig 2FA-løkke. Alt er nå verifisert grundig og rullet ut, eksisterende sesjoner er upåvirket.
2026-07-19 10:40:15 +02:00
**ADR-021 er dermed helt ferdig.** Se CLAUDE.md-status for full byggerunde,
inkl. tre reelle bugs funnet og fikset under scratch-testing.
2026-07-19 10:05:42 +02:00
### Organisasjonseierskap: dele, invitere, frasi seg, superadmin — ADR-022
Reist rett etter ADR-021. Avdekket et bredere, mer fundamentalt hull enn
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
ADR-021 (passord/2FA) og ADR-022 (org-eierskap) er live. Kort oppsummert:
Nytt i innloggingen:
Passord (valgfritt tillegg til magic-link) — Argon2id, testet med ekte spesialtegn/mellomrom/æøå
2FA — TOTP eller e-post-engangskode, brukerens eget valg
Påkrevd 2FA for org-eiere/administratorer, valgfritt for andre
Ny /account-skjerm for å sette passord og styre 2FA
Nytt for organisasjoner:
Inviter andre på e-post til enhver rolle (eiere) eller kun medlem (administratorer)
Frasi deg eierskap selv, eller fjern andre medlemmer — med vern mot at en organisasjon står igjen uten eier
Superadmin-flagg (kun manuelt satt i databasen, aldri via API) som kan sette eierskap på hvilken som helst organisasjon
Ny /orgs/[id]/members-skjerm, lenket fra dashbordet
Fant og fikset tre reelle bugs underveis i scratch-testingen (ingen nådde produksjon) — en krasj i selve innloggingen og to tilfeller av en uendelig 2FA-løkke. Alt er nå verifisert grundig og rullet ut, eksisterende sesjoner er upåvirket.
2026-07-19 10:40:15 +02:00
bare "del eierskap": det fantes tidligere INGEN vei til å legge til et
2026-07-19 10:05:42 +02:00
organisasjonsmedlem etter opprettelse i det hele tatt — kun grunnleggerens
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
ADR-021 (passord/2FA) og ADR-022 (org-eierskap) er live. Kort oppsummert:
Nytt i innloggingen:
Passord (valgfritt tillegg til magic-link) — Argon2id, testet med ekte spesialtegn/mellomrom/æøå
2FA — TOTP eller e-post-engangskode, brukerens eget valg
Påkrevd 2FA for org-eiere/administratorer, valgfritt for andre
Ny /account-skjerm for å sette passord og styre 2FA
Nytt for organisasjoner:
Inviter andre på e-post til enhver rolle (eiere) eller kun medlem (administratorer)
Frasi deg eierskap selv, eller fjern andre medlemmer — med vern mot at en organisasjon står igjen uten eier
Superadmin-flagg (kun manuelt satt i databasen, aldri via API) som kan sette eierskap på hvilken som helst organisasjon
Ny /orgs/[id]/members-skjerm, lenket fra dashbordet
Fant og fikset tre reelle bugs underveis i scratch-testingen (ingen nådde produksjon) — en krasj i selve innloggingen og to tilfeller av en uendelig 2FA-løkke. Alt er nå verifisert grundig og rullet ut, eksisterende sesjoner er upåvirket.
2026-07-19 10:40:15 +02:00
egen `owner` -rad ble noensinne satt inn.
2026-07-19 10:05:42 +02:00
| Del | Status | Notat |
|---|---|---|
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
ADR-021 (passord/2FA) og ADR-022 (org-eierskap) er live. Kort oppsummert:
Nytt i innloggingen:
Passord (valgfritt tillegg til magic-link) — Argon2id, testet med ekte spesialtegn/mellomrom/æøå
2FA — TOTP eller e-post-engangskode, brukerens eget valg
Påkrevd 2FA for org-eiere/administratorer, valgfritt for andre
Ny /account-skjerm for å sette passord og styre 2FA
Nytt for organisasjoner:
Inviter andre på e-post til enhver rolle (eiere) eller kun medlem (administratorer)
Frasi deg eierskap selv, eller fjern andre medlemmer — med vern mot at en organisasjon står igjen uten eier
Superadmin-flagg (kun manuelt satt i databasen, aldri via API) som kan sette eierskap på hvilken som helst organisasjon
Ny /orgs/[id]/members-skjerm, lenket fra dashbordet
Fant og fikset tre reelle bugs underveis i scratch-testingen (ingen nådde produksjon) — en krasj i selve innloggingen og to tilfeller av en uendelig 2FA-løkke. Alt er nå verifisert grundig og rullet ut, eksisterende sesjoner er upåvirket.
2026-07-19 10:40:15 +02:00
| 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).
2026-07-19 10:05:42 +02:00
---
PWA er bygget og scratch/build-verifisert. Status:
Bygget:
Installerbar app: app/manifest.ts, appleWebApp-metadata for iOS, service worker (public/sw.js, håndskrevet — ingen next-pwa-avhengighet), public/offline.html.
Ikoner: enkelt grønt golf-flagg generert programmatisk (måtte omgå at verken PIL, rsvg-convert eller ImageMagick fantes i miljøet — løste det med et scratch npm-oppsett av sharp). Midlertidig, notert i FEATURE_BACKLOG.md for senere erstatning, slik du ba om. Erstattet samtidig den gamle apple-icon.png som faktisk var v0.app sin generiske plassholderlogo, ikke TeeCup-merkevare.
Full offline scoreregistrering: ny lib/offline-queue.ts (IndexedDB-kø) koblet inn i scorekort-skjermen. Slag/hull-resultat registrert offline vises umiddelbart som "Lagret lokalt · venter på synk", synkes automatisk når nettet er tilbake (pluss en manuell "Synkroniser nå"-knapp). Se ADR-028 for alle designvalgene (bl.a. hvorfor køen ligger i klientkoden og ikke i service workeren, og hvorfor jeg bevisst unngikk Background Sync API — iOS Safari støtter den ikke).
Viktig å være ærlig om: jeg har ingen nettleser-verktøy tilgjengelig i denne økten, så offline-flyten er verifisert med typesjekket produksjonsbuild + en kort container-boot (curl mot manifest/service worker/ikoner — alle svarer riktig), men ikke faktisk testet i en ekte nettleser (Chrome DevTools sin Offline-bryter, ordentlig "Legg til på hjemskjerm"). Anbefaler sterkt at du selv tester scorekort-siden med DevTools offline-modus før dere stoler på det i skarp bruk.
De fire UI-hullene du meldte inn er dokumentert i FEATURE_BACKLOG.md/CLAUDE.md med root cause (ikke bare notert som klager) — den mest presserende er /orgs/{id}/members, som er en reell rewrite/dynamisk-rute-kollisjon: siden er helt utilgjengelig i dag fordi Next.js sin /orgs/:path*-proxy-regel fanger kallet før selve siden noensinne nås.
Ingen migrasjon i denne runden — kun teecup_frontend trenger redeploy.
2026-07-19 23:51:08 +02:00
## Rapporterte UI-/UX-hull (2026-07-19) — notert, IKKE fikset ennå
Fire punkter rapportert av brukeren fra faktisk bruk av `teecup.teeoff.no` .
Root cause funnet ved kodegjennomgang for de tre første (ikke bare gjettet);
det fjerde er et reelt manglende UI-element, ingen kodefeil. Ingen av de fire
er fikset i denne runden — kun dokumentert slik at de ikke går i glemmeboken.
1. **Dashboard-turneringskortet viser "Ingen datoer satt" selv om øktene
(rundene) har dato/klokkeslett satt.** `components/tournament-card.tsx`
leser `tournament.start_date` /`end_date` — et EGET, frittstående felt på
selve turneringen (ADR-015), atskilt fra `session.scheduled_at` (dato per
økt/runde). Grep bekrefter: disse to tournament-feltene LESES tre steder
i frontend (`dashboard.tsx`, `public-tournament.tsx` , `public-club.tsx` ),
men skrives INGEN steder — det finnes ingen UI for å sette dem i det hele
tatt. Kortet vil derfor alltid vise "ingen datoer satt", uansett hvor mange
økter som har fått en `scheduled_at` , fordi feltet det leser aldri kan bli
satt gjennom UI-et. To mulige retninger: (a) bygg et faktisk
`start_date` /`end_date`-skjemafelt på turneringen, eller (b) la kortet
utlede visningsdatoen fra øktenes `scheduled_at` -spenn i stedet for et
eget, separat felt — sistnevnte er trolig det organisatoren faktisk
forventer.
2. ** `/orgs/{id}/members` -siden gir en rå API-404 (`{"detail":"Not
Found"}`), ikke medlemssiden.** Bekreftet root cause, ikke bare
reprodusert: `frontend/next.config.mjs` sin `rewrites()` returnerer en
PLAIN ARRAY (implisitt "afterFiles"-semantikk i Next.js) — det betyr at
ikke-dynamiske filer/sider sjekkes FØR rewrites, men DYNAMISKE sider
(som `app/orgs/[id]/members/page.tsx` ) sjekkes ETTER. Rewrite-regelen
`{ source: "/orgs/:path*", destination: ".../orgs/:path*" }` (satt opp i
ADR-016 for å proxye API-kall) fanger derfor `/orgs/{id}/members` FØR
Next.js noensinne når frem til den faktiske siden, og sender kallet til
FastAPI i stedet — som naturligvis ikke har noen `GET /orgs/{id}/members` -
rute (kun `/orgs/{id}/memberships` ), derav den rå FastAPI-404-formen
(ikke engang appens egen `app_error` -kontrakt, siden ruten ikke matcher
noe sted i det hele tatt). Selve siden (`org-members.tsx`, lenken fra
dashbordet) er ellers riktig bygget — dette er en ren
rewrite/dynamisk-rute-presedens-krasj, samme klasse fallgruve som
ADR-016 sin opprinnelige "alt nytt API-prefiks må inn i rewrites"-lærdom,
bare i motsatt retning (en frontend-SIDE ble skjult AV en rewrite). Dette
er den FØRSTE frontend-siden som noensinne har blitt nestet direkte under
et allerede proxyet prefiks (`/orgs/*`) — ingen tidligere skjerm har
truffet dette. Sannsynlig fiks: flytt siden til en ikke-proxyet sti
(f.eks. `/organizations/[id]/members` ), ELLER gjør rewrites-regelen mer
presis (kun kjente API-undermønstre som `/orgs/:id/tournaments` ,
`/orgs/:id/memberships` osv., ikke et bredt `:path*` ).
3. **Dashboard: ingen vei til å opprette/legge til en ANDRE organisasjon.**
Bekreftet i `dashboard.tsx` : `CreateOrganizationState`
(opprett-organisasjon-skjemaet) vises KUN når `hasOrg` er `false` , altså
når brukeren har null organisasjoner fra før. Har brukeren allerede én
org, finnes ingen knapp/lenke noe sted i UI-et for å opprette en til —
selv om backend-et støtter dette fullt ut og uten begrensning (ADR-021,
bekreftet: `POST /orgs` har ingen grense på antall org-er én bruker kan
eie). Ren manglende UI, ikke en backend-begrensning.
4. **Ingen måte å se, på ett blikk, at ALLE runder/økter i en turnering har
fått dato/klokkeslett satt.** `tournament-program.tsx` viser
`scheduled_at` per øktkort hvis satt, ingenting spesielt (ingen
fremhevet "mangler dato"-tilstand) hvis ikke. Ingen sammendrag/telling
noe sted ("X av Y runder har dato") — organisatoren må åpne
program-skjermen og lese hvert kort manuelt. Ren UX-mangel, ingen
bakenforliggende datamodell-begrensning (all nødvendig data finnes
allerede i `GET .../sessions` ).
**Ingenting av dette er fikset ennå** — kun diagnostisert og notert på
brukerens eksplisitte instruks, for å ikke gå i glemmeboken mens PWA-runden
prioriteres.
---
2026-07-16 07:18:01 +02:00
## UX / frontend (senere fase)
- 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp
(banebruk i sollys/med solbriller).
PWA er bygget og scratch/build-verifisert. Status:
Bygget:
Installerbar app: app/manifest.ts, appleWebApp-metadata for iOS, service worker (public/sw.js, håndskrevet — ingen next-pwa-avhengighet), public/offline.html.
Ikoner: enkelt grønt golf-flagg generert programmatisk (måtte omgå at verken PIL, rsvg-convert eller ImageMagick fantes i miljøet — løste det med et scratch npm-oppsett av sharp). Midlertidig, notert i FEATURE_BACKLOG.md for senere erstatning, slik du ba om. Erstattet samtidig den gamle apple-icon.png som faktisk var v0.app sin generiske plassholderlogo, ikke TeeCup-merkevare.
Full offline scoreregistrering: ny lib/offline-queue.ts (IndexedDB-kø) koblet inn i scorekort-skjermen. Slag/hull-resultat registrert offline vises umiddelbart som "Lagret lokalt · venter på synk", synkes automatisk når nettet er tilbake (pluss en manuell "Synkroniser nå"-knapp). Se ADR-028 for alle designvalgene (bl.a. hvorfor køen ligger i klientkoden og ikke i service workeren, og hvorfor jeg bevisst unngikk Background Sync API — iOS Safari støtter den ikke).
Viktig å være ærlig om: jeg har ingen nettleser-verktøy tilgjengelig i denne økten, så offline-flyten er verifisert med typesjekket produksjonsbuild + en kort container-boot (curl mot manifest/service worker/ikoner — alle svarer riktig), men ikke faktisk testet i en ekte nettleser (Chrome DevTools sin Offline-bryter, ordentlig "Legg til på hjemskjerm"). Anbefaler sterkt at du selv tester scorekort-siden med DevTools offline-modus før dere stoler på det i skarp bruk.
De fire UI-hullene du meldte inn er dokumentert i FEATURE_BACKLOG.md/CLAUDE.md med root cause (ikke bare notert som klager) — den mest presserende er /orgs/{id}/members, som er en reell rewrite/dynamisk-rute-kollisjon: siden er helt utilgjengelig i dag fordi Next.js sin /orgs/:path*-proxy-regel fanger kallet før selve siden noensinne nås.
Ingen migrasjon i denne runden — kun teecup_frontend trenger redeploy.
2026-07-19 23:51:08 +02:00
- ✅ 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 2026-07-19 (ADR-028), IKKE ENNÅ RULLET UT
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) | ❌ IKKE gjort | Kun verifisert med typesjekket build + container-boot/curl. Ingen nettleserverktøy tilgjengelig denne runden — **anbefales sterkt at brukeren selv tester offline-flyten i Chrome DevTools før tillit i skarp bruk.** |
**Ikke rullet ut ennå** — venter på brukerens eksplisitte
utrullingsbekreftelse (ingen migrasjon, kun `teecup_frontend` ).
2026-07-16 07:18:01 +02:00
---
## 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).