788 lines
52 KiB
Markdown
788 lines
52 KiB
Markdown
# CLAUDE.md — arbeidsinstruks for TeeCup
|
||
|
||
Les dette først i hver økt. Det koder hva vi har bestemt og hvordan vi jobber.
|
||
|
||
## Autoritative kilder (les før du gjør noe)
|
||
- `ARCHITECTURE_DECISIONS.md` — hva som er bestemt og hvorfor (ADR-001…018). Fasit.
|
||
- `FEATURE_BACKLOG.md` — hva som gjenstår, hva som er utsatt, hva som mangler.
|
||
- Endres en beslutning: legg til en ny ADR, ikke slett historikk. Hold begge
|
||
filene oppdatert når noe avgjøres.
|
||
|
||
## Sikkerhetsregler (ufravikelige)
|
||
- Rør ALDRI `teeoff`-databasen eller den ekte `teecup_db` uten at brukeren
|
||
eksplisitt har bekreftet det i samme økt. Test alltid migrasjoner mot en egen
|
||
scratch-database først, og rydd opp etterpå.
|
||
- Vis planen (hvilke kommandoer, mot hvilken database) FØR du kjører noe som
|
||
skriver, migrerer eller sletter. Vent på bekreftelse.
|
||
- Hemmeligheter (passord, secrets) bor i `.env` (filrettigheter 600), dekkes av
|
||
`.gitignore`, committes aldri, og skrives aldri i klartekst i chatten eller i
|
||
SQL-filer. Generer dem på serveren (`openssl rand -base64 32`).
|
||
- Kjør appen som databaserollen `teecup_app` (NOSUPERUSER, NOBYPASSRLS) — aldri
|
||
som `teeoff_admin`/superuser i runtime.
|
||
|
||
## Arkitektur-invarianter (ikke bryt uten en ny ADR)
|
||
- Tenant = organisasjon. `organization_id` på alle domenetabeller, håndhevet av
|
||
RLS. App-koden setter `app.current_org` med `SET LOCAL` per transaksjon.
|
||
- Verifiser at brukeren er medlem av organisasjonen FØR org-konteksten settes.
|
||
RLS stoler blindt på `app.current_org`.
|
||
- Egen innlogging (uavhengig av teeoff). Banedata hentes fra teeoff via lesende
|
||
API, ikke delt database.
|
||
- v1 = nøyaktig to lag (Ryder Cup-format), håndhevet i app-laget. Match-modellen
|
||
holdes generell (to sider) så knockout/flere lag kan komme senere.
|
||
- Handicap-/matchlogikk skal ligge i `handicap_engine.py` (rent, testet, uten
|
||
db/API-avhengigheter). Allowances er konfig, ikke hardkodet.
|
||
- Media (bilder/video) skal i objektlagring (MinIO), ikke i Postgres. Postgres
|
||
holder bare metadata + nøkkel.
|
||
|
||
## Arbeidsmåte
|
||
- Inkrementelt. Ingenting tas for gitt før det er testet. Bekreft hvert steg før
|
||
du går videre.
|
||
- Bruk git (remote: brukerens Forgejo). Commit i logiske steg med tydelige
|
||
meldinger.
|
||
- Er du usikker på omfang eller en beslutning: spør heller enn å gjette.
|
||
|
||
## Status (oppdater denne når ting endres)
|
||
Ferdig og verifisert:
|
||
- Handicap-motor + tester (24/24, R&A-verifisert).
|
||
- Skjema `001` + roller `002` + scoring/blind draw `003`. Isolasjon bevist med
|
||
`test_isolation.sql` (RLS-oppførsel, ikke bare at skjemaet kjører).
|
||
- 002 hadde en reell bug (psql interpolerer ikke `:'var'` inne i `DO $$...$$`)
|
||
— permanent fikset, verifisert mot scratch to ganger.
|
||
- API-et kjørt for ekte (ikke bare syntaks-sjekket) i en engangs Docker-
|
||
container mot en scratch-database, RLS bevist gjennom hele
|
||
asyncpg-pool-stacken (ikke bare i rå SQL).
|
||
- Oppsett-endepunktene er bygget og verifisert: `app/routers/players.py`
|
||
(spillerpool), `tournaments.py` (turnering/lag/roster/økter, ADR-011
|
||
to-lags-grense håndhevet med `FOR UPDATE`-lås), `matches.py` (matcher/
|
||
deltakere/blind draw-lås, ADR-013-synlighet push-down i SQL). Delt
|
||
feiloversettelse i `app/errors.py`, delte synlighetsspørringer i
|
||
`app/blind_draw.py`. `main.py` er nå bare app-factory + `include_router`.
|
||
- Scoring-runden er bygget og verifisert for ekte mot scratch-db (18-hulls
|
||
bane med `tee_rating`, 4 spillere for fourball-testing): `app/handicap.py`
|
||
(ADR-014 fire brytere via `parse_allowance_config`, handicap beregnes i
|
||
`compute_and_store_side_handicaps` rett etter deltaker-innsetting —
|
||
singles/fourball per spiller umiddelbart, foursome/greensome/scramble kun
|
||
når siden er komplett), `app/routers/scoring.py` (`hole-scores`/
|
||
`hole-results`-upsert, `scorecard`-GET, matchstatus-recompute med
|
||
`FOR UPDATE`-lås mot race og SAMMENHENGENDE-prefiks-regel for uferdige
|
||
hull). `app/team_authz.py` skilt ut fra `matches.py` (delt med
|
||
`scoring.py`). Alle 10 planlagte tester bestått, inkl. fourball
|
||
better-ball-aggregering (MIN av to nettoer, venter til begge partnere har
|
||
registrert), poeng-caching ved tidlig avgjort match, og ADR-014-bryteren
|
||
`use_handicap=false`.
|
||
**Fant og fikset underveis:** `tournaments.py` sin `SessionCreate` manglet
|
||
`scoring_mode` helt (økter kunne aldri opprettes i `hole_result`-modus via
|
||
API-et) — lagt til.
|
||
**Bevisst utelatt/kjente begrensninger:** en side som aldri når forventet
|
||
deltakerantall (no-show) får aldri beregnet handicap og matchen kan da
|
||
aldri avgjøres — ingen manuell overstyring bygget. Score-skriving er
|
||
upsert (ingen avvisning ved duplikat) — ingen audit-trail på rettelser.
|
||
Kapteins-only autorisasjon fortsatt ikke bygget (FEATURE_BACKLOG ❓); bar
|
||
er «rostret på laget».
|
||
- **Match-lås ved avgjørelse (2026-07-16):** `submit_hole_score`/
|
||
`submit_hole_result` avviser nå 409 hvis `match.points_side_a IS NOT NULL`
|
||
(matchen er avgjort) — FØR upserten kjøres, både for nye hull og
|
||
korrigering av allerede talte hull. Tetter en reell bug: uten dette kunne
|
||
«spøkelses-hull» lagt inn etter avgjørelse endre en allerede cachet margin
|
||
ved neste omregning. Automatisk, ingen ny autorisasjon involvert.
|
||
- **Ekte autentisering bygget og verifisert (2026-07-16):** `X-Debug-User-Id`-
|
||
stubben er HELT fjernet (ingen fallback). Magic-link + JWT-sesjon i
|
||
`app/routers/auth.py` + `app/auth.py` (`request-link`/`verify-link`/
|
||
`logout`/`me`), ny migrasjon `004_auth.sql` (`magic_link_token`-tabell +
|
||
unik e-post-indeks på `app_user`). Token = `secrets.token_urlsafe(32)`, kun
|
||
SHA-256-hash lagres, atomisk forbruk (`UPDATE ... RETURNING`, ikke
|
||
les-sjekk-skriv), generisk respons uansett om e-posten finnes (unngår
|
||
enumerering), gamle uforbrukte lenker ugyldiggjøres når en ny utstedes,
|
||
`app_user` opprettes FØRST ved vellykket verifisering (ikke ved
|
||
forespørsel). Sesjons-JWT (PyJWT, `algorithms=["HS256"]` eksplisitt) i
|
||
HttpOnly/SameSite=Lax/dynamisk-Secure-cookie, 30 dager, med et ekte
|
||
eksistens-oppslag mot `app_user` på hver forespørsel (faktisk
|
||
tilbakekalling — en slettet bruker kan ikke ri ut sesjonen). Alle 12
|
||
planlagte tester bestått.
|
||
**Fant og fikset underveis:** `ON CONFLICT (email)` matchet ikke den nye
|
||
PARTIELLE unike indeksen uten eksplisitt `WHERE email IS NOT NULL` (samme
|
||
klasse feil som `hole_score`s partielle indekser i scoring-runden).
|
||
**Fant, IKKE fikset i denne runden (egen runde rett etterpå — se under):**
|
||
`organization`-tabellens RLS-policy kastet en 500 i stedet for skjemaets
|
||
lovede "trygg standard: se ingenting" ved tomstreng-GUC.
|
||
- **RLS-tomstreng-bug FIKSET (2026-07-16):** ny migrasjon
|
||
`005_rls_null_guard.sql` — delt `STABLE` SQL-funksjon `app_current_org()`
|
||
gjør `NULLIF(current_setting('app.current_org', true), '')::uuid` i stedet
|
||
for det rå uttrykket, brukt av alle 15 RLS-policyer (`ALTER POLICY`,
|
||
14 `org_isolation` + `org_self`). Verifisert med 3 nye regresjonstester i
|
||
`test_isolation.sql` (Test 10-12) OG ved faktisk å gjenskape original-
|
||
buggen mot en ekte container (pool-størrelse 1, varm opp med
|
||
`org_connection()`, deretter `/auth/me` på samme gjenbrukte tilkobling —
|
||
gikk fra 500 til 200).
|
||
**Viktig presisering fra denne runden:** fiksen gjør IKKE at `/auth/me` kan
|
||
joine `organization` direkte via `plain_connection()` — det var en feilaktig
|
||
antakelse i forrige runde. `org_self` krever fortsatt en MATCHENDE
|
||
`app.current_org` for å vise en rad (riktig RLS-design, ikke noe fiksen
|
||
skulle endre), og 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,
|
||
N = antall org-er brukeren tilhører) — dette er riktig løsning, ikke en
|
||
omvei.
|
||
- **Organisasjon-bootstrap bygget og verifisert (2026-07-16):** nytt
|
||
`POST /orgs` (`app/routers/organizations.py`) — det ENESTE stedet i API-et
|
||
som setter inn en `organization`-rad. Fant under statusgjennomgang at dette
|
||
manglet helt (alle tidligere org-er var seedet med superbruker-SQL). Ingen
|
||
ny migrasjon. Selvrefererende RLS-bootstrap bekreftet å fungere: generer
|
||
org-ens uuid i Python, sett `app.current_org` til nøyaktig den via
|
||
eksisterende `org_connection()`, sett inn `organization`-raden med samme
|
||
id — `org_self`s implisitte `WITH CHECK` blir da trivielt sann, ingen
|
||
privilegert tilkobling nødvendig (i motsetning til hva 002s kommentar
|
||
antydet). Verifisert med 5 tester inkl. en negativ kontroll (mismatchende
|
||
id avvist med `insufficient_privilege`) og full kryss-org-isolasjon mellom
|
||
to uavhengig opprettede organisasjoner.
|
||
- **Ekte SMTP-utsending bygget og verifisert (2026-07-16):** ny `app/email.py`
|
||
(`send_magic_link_email`, `smtplib` via `asyncio.to_thread`, håndterer
|
||
både implisitt TLS/port 465 og STARTTLS dynamisk). Brukeren la egne
|
||
SMTP-credentials i `.env` (`TEECUP_SMTP_*`, `TEECUP_FROM_EMAIL` — ADR-009,
|
||
ikke delt med teeoff); jeg leste kun nøkkelnavnene for å bekrefte de
|
||
fantes, aldri verdiene. `app/config.py` sin `SMTP_CONFIGURED` er valgfri
|
||
(ikke `_required`) — dev-only logging (`TEECUP_DEV_LOG_MAGIC_LINKS`)
|
||
fortsatt fungerer uendret når SMTP ikke er satt opp. Driftsfeil i
|
||
utsendingen lekker aldri til klientresponsen (bevarer anti-enumerering).
|
||
**Verifisert med faktisk levering:** sendte én ekte test-e-post til en
|
||
adresse brukeren oppga — brukeren bekreftet mottak. Første gang noe i
|
||
prosjektet er bevist ved ekte levering, ikke bare curl/scratch.
|
||
- **Containerisert og LIVE på `teecup.teeoff.no` (2026-07-16):** ekte
|
||
`teecup_db` opprettet (migrasjoner 001→005 kjørt permanent, `test_isolation.sql`
|
||
består), `Dockerfile` + `docker-compose.yml` (tjeneste `teecup_api`, joiner
|
||
det eksisterende `teeoff_default`-nettverket), Caddy-blokk lagt til i
|
||
`/opt/teeoff/deploy/Caddyfile`. Ekte innlogging (magic-link → e-post →
|
||
JWT-sesjon med `Secure`-cookie) verifisert ende-til-ende mot den live
|
||
stacken. `teeoff.no` upåvirket gjennom hele prosessen.
|
||
**To reelle hendelser underveis, begge løst:**
|
||
1. **Caddy plukket ikke opp filendringen** — `teeoff_caddy` sin
|
||
`Caddyfile`-mount er en ENKELTFIL-bind-mount, låst til inoden som fantes
|
||
da containeren sist startet. Min fil-redigering (atomisk rename) laget
|
||
en ny inode på samme sti, så containeren fortsatte å lese den GAMLE
|
||
filen uansett hvor mange ganger `caddy validate`/`caddy reload`/admin-API
|
||
`/load` ble kjørt (alle validerte/lastet den uendrede gamle filen, derav
|
||
ingen feilmelding). Løst med en full `docker restart teeoff_caddy`
|
||
(brukeren bekreftet — avvek fra planens "kun graceful reload, ingen
|
||
omstart"-løfte, noen sekunders nedetid for `teeoff.no`).
|
||
2. **Alvorlig nettverksalias-kollisjon** (funnet RETT ETTER omstarten, da
|
||
ekte teeoff-trafikk som `/api/facilities?...` med ekte klubb-slugs dukket
|
||
opp i `teecup_api` sin logg): `docker-compose.yml` sin service-nøkkel var
|
||
`api:` — SAMME nøkkel som teeoffs eget `api`-servicenavn
|
||
(`docker-compose.prod.yml`). Docker Compose registrerer nettverksalias
|
||
basert på service-NAVNET (ikke bare `container_name`) på delte nettverk,
|
||
så BEGGE containerne fikk alias `api` på `teeoff_default` — Caddys
|
||
`reverse_proxy api:8000` i teeoff sin egen config kunne da tilfeldig
|
||
treffe enten ekte `teeoff_api` eller `teecup_api`. **Rettet umiddelbart**
|
||
(stoppet `teecup_api` først for å hindre videre feilruting av ekte
|
||
teeoff-trafikk, ga service-nøkkelen navnet `teecup_api` i stedet,
|
||
gjenopprettet — bekreftet med `docker network inspect` at alias `api` nå
|
||
KUN peker på ekte `teeoff_api`).
|
||
**Mindre driftslærdom:** (a) jeg eksponerte ved et uhell
|
||
`TEECUP_SMTP_PASS`/`TEECUP_FROM_EMAIL` i eget debug-output mens jeg
|
||
feilsøkte en `.env`-korrupsjon (manglende linjeskift fra min egen
|
||
`>>`-tilføyelse) — brukeren roterte passordet som forsiktighetsregel; (b)
|
||
Docker leser IKKE `.env` på nytt for en allerede kjørende container —
|
||
`docker compose up -d --force-recreate` kreves etter enhver `.env`-endring
|
||
som skal tas i bruk; (c) et `#`-tegn i et upassordet `.env`-passord kuttes
|
||
som en kommentar av Compose sin parser — anførselstegn (fortrinnsvis enkle)
|
||
løser dette.
|
||
- **Dato/klokkeslett + feilkode-kontrakt + i18n-forberedelse (2026-07-17,
|
||
ADR-015):** reist av brukeren rett før frontend-arbeidet. Ny migrasjon
|
||
`006_scheduling_and_locale.sql` (alt additivt): `tournament.end_date`,
|
||
`session.scheduled_at`/`tee_interval_minutes`/`start_hole`,
|
||
`match.tee_time_override`, `app_user.preferred_locale`,
|
||
`magic_link_token.locale`. `match.tee_time` er UTLEDET i Python
|
||
(`scheduled_at + (sequence-1)*tee_interval_minutes`, override vinner hvis
|
||
satt) — aldri lagret per match. Full retrofit av ALLE 39 daværende
|
||
`HTTPException(..., detail="norsk streng")`-steder på tvers av 6 filer til
|
||
en delt `app_error(status_code, code, message)`-factory
|
||
(`app/errors.py`) — responsformen er nå konsekvent
|
||
`{"detail":{"code":...,"message":...}}` i hele API-et, verifisert med et
|
||
siste `grep -rn 'detail="' app/` som ga NULL treff. i18n: `locale`
|
||
(`nb`/`en`) sendes av klienten ved `request-link`, styrer e-postmalen
|
||
(ekte engelsk mal lagt inn i `app/email.py`, ikke bare rørlegging) OG
|
||
settes som en HELT NY brukers `preferred_locale` — en eksisterende bruker
|
||
som logger inn på et annet språk får IKKE sin lagrede preferanse
|
||
overskrevet.
|
||
**Fant og fikset underveis:** `tournament.start_date` har ligget i
|
||
skjemaet siden migrasjon 001, men var ALDRI koblet til
|
||
`TournamentCreate`/`Tournament`-modellene — funnet som en naturlig
|
||
bivirkning av å legge til `end_date`.
|
||
**Verifisert grundig mot fersk `teecup_scratch`** (001→006,
|
||
`test_isolation.sql` fortsatt 12/12): feilkode-form bekreftet på tvers av
|
||
5 filer (`NOT_FOUND`/`LIMIT_REACHED` tournaments.py, `DUPLICATE` roster,
|
||
`NOT_AUTHENTICATED` uten cookie, `NOT_ROSTERED_ON_TEAM` matches.py),
|
||
tee_time-beregning bekreftet (10 min intervall → 08:00/08:10/08:20,
|
||
override vinner, `scheduled_at=null` gir `tee_time:null` ikke feil), full
|
||
i18n-runde bekreftet (ny bruker `locale:"en"` → `preferred_locale:"en"`;
|
||
påfølgende `request-link` for samme bruker med `locale:"nb"` skiftet
|
||
e-postmalen men IKKE den lagrede preferansen, bekreftet via `/auth/me`).
|
||
**Kjørt mot ekte `teecup_db` 2026-07-17** (bruker bekreftet eksplisitt i
|
||
samme økt): migrasjonen kjørte rent, `test_isolation.sql` fortsatt 12/12
|
||
(ruller alltid tilbake, ingen domenerader berørt).
|
||
**Mindre driftslærdom, funnet OG rettet samme runde:** et `.env`-filter
|
||
(`grep -v -i 'pass|secret|key'`) jeg brukte for å lese ikke-sensitive
|
||
nøkler fanget ikke opp `TEECUP_DATABASE_URL`, som bar `teecup_app`-
|
||
passordet innebygd i selve URL-en (i tillegg til at det SAMME passordet
|
||
allerede lå rent i `TEECUP_APP_PASSWORD` — duplisert, ikke bare skjult) —
|
||
passordet ble dermed synlig i et verktøyresultat. Flagget til brukeren
|
||
umiddelbart (samme mønster som SMTP-passord-hendelsen over).
|
||
- **`.env`/tilkobling ryddet opp (2026-07-17), samme runde:** roten til
|
||
hendelsen over var at `TEECUP_DATABASE_URL` var én sammensatt DSN-streng
|
||
med passordet URL-kodet inni — usynlig for navnebaserte secret-filtre.
|
||
Erstattet med fem separate, rent navngitte felt:
|
||
`TEECUP_DB_HOST`/`TEECUP_DB_PORT`/`TEECUP_DB_NAME`/`TEECUP_DB_USER`/
|
||
`TEECUP_DB_PASS` (sistnevnte omdøpt fra `TEECUP_APP_PASSWORD`, kun
|
||
nøkkelnavnet — verdien aldri lest eller skrevet av meg). `app/config.py`
|
||
og `app/db.py` bygger nå `asyncpg`-poolen fra disse fem separate feltene
|
||
(`host=`/`port=`/`user=`/`password=`/`database=`) i stedet for én DSN —
|
||
fjerner også URL-prosentkoding-problemet fra SMTP-passord-hendelsen sin
|
||
klasse av feil. `docker-compose.yml` sin `environment:`-liste oppdatert
|
||
tilsvarende. **Krevde et fullt image-rebuild, ikke bare
|
||
`--force-recreate`:** `Dockerfile` sin `COPY app/ app/` bakes inn i
|
||
imaget ved build-tid (ingen bind-mount i prod, i motsetning til
|
||
scratch-verifiseringens engangscontainere) — en ren `--force-recreate`
|
||
gjenbrukte det GAMLE imaget og krasjet umiddelbart på den nå fjernede
|
||
`TEECUP_DATABASE_URL`. Rettet med `docker compose up -d --build
|
||
--force-recreate`. **Verifisert ende-til-ende mot den live stacken:**
|
||
containeren boot-et rent (`Application startup complete` i loggen, som
|
||
krever en vellykket `init_pool()` — asyncpg ville kastet og forhindret
|
||
akkurat den logglinjen ved feil tilkoblingsparametre), `/auth/me` over
|
||
ekte https ga et rent `401 NOT_AUTHENTICATED` (ikke 500/502), `teeoff.no`
|
||
upåvirket (`200` gjennom hele omstarten, kun `teecup_api` restartet — ikke
|
||
delt Caddy-instans, ikke samme risikoklasse som Caddy-hendelsen fra
|
||
containeriseringsrunden).
|
||
|
||
- **Frontend startet, innlogging LIVE (2026-07-17, ADR-016):** første
|
||
frontend-skjerm i prosjektet. Designet i V0 (Next.js + Tailwind +
|
||
shadcn/ui), hentet inn som `frontend/` — merkevare-form/farge fra
|
||
Teeoff-logoen, IKKE navn/logo (egne, separate produkter, se ADR-009).
|
||
**Kvalitetsrunde før bruk:** V0s fargetokens var OKLCH-TILNÆRMINGER, ikke
|
||
eksakte — regnet ut presise verdier fra `#8bc24a`/`#ff5722` og rettet alle
|
||
6 forekomster i `globals.css`. Fjernet `@vercel/analytics` (unødvendig på
|
||
egen-hostet infra), fjernet `typescript: { ignoreBuildErrors: true }`
|
||
(ekte typesjekk kjører nå), fjernet dødt `pnpm.overrides`-felt.
|
||
`frontend/.gitignore` manglet `.pnpm-store/` — årsaken til at brukerens
|
||
VSCode viste 10 000 "ukjente" filer ved første commit-forsøk; rettet.
|
||
**Kablet mot ekte API:** `next.config.mjs` sin `rewrites()` proxyer
|
||
`/auth/*`/`/orgs/*`/`/health` server-side til `teecup_api` — same-origin,
|
||
ingen CORS, cookie uendret (full begrunnelse i ADR-016). Login-skjermet
|
||
sender ekte `POST /auth/request-link`; ny `/verify`-side mottar
|
||
`?token=...` fra e-postlenken (auto-verifiserer) eller viser et manuelt
|
||
"lim inn koden"-felt. `app/email.py` fikk en ny `PUBLIC_BASE_URL`-
|
||
innstilling og sender nå en EKTE klikkbar lenke (koden beholdes som
|
||
fallback).
|
||
**Reell fallgruve funnet og fikset ved containerisering:** Next.js sin
|
||
`rewrites()` løses ved BUILD-tid for `output: "standalone"`, ikke ved
|
||
container-oppstart — en runtime `-e TEECUP_API_ORIGIN=...` ble stille
|
||
ignorert (proxy-kall feilet med `ECONNREFUSED` mot `localhost:8000`).
|
||
Løst med en Docker build-time `ARG TEECUP_API_ORIGIN` i
|
||
`frontend/Dockerfile`, satt via `docker-compose.yml` sin `build.args`.
|
||
**Rullet ut live:** ny `teecup_frontend`-tjeneste i `docker-compose.yml`.
|
||
Caddy (`teecup.teeoff.no`, i det SEPARATE `teeoff`-repoet,
|
||
`/opt/teeoff/deploy/Caddyfile`) endret fra å peke direkte på `teecup_api`
|
||
til å peke på `teecup_frontend` — samme stale-inode-oppførsel som
|
||
containeriseringsrunden (graceful `reload` plukket IKKE opp endringen,
|
||
`/` fortsatte å gi `teecup_api` sin egen 404 i stedet for innloggingssiden
|
||
til reload faktisk skjedde). Løst likt: full `docker restart
|
||
teeoff_caddy`, brukeren bekreftet eksplisitt på forhånd. `teeoff.no`
|
||
upåvirket gjennom hele omstarten.
|
||
**Verifisert med FAKTISK e-postlevering:** ekte magic-link sendt til
|
||
brukerens egen adresse over `https://teecup.teeoff.no`, ekte e-post
|
||
mottatt med en ekte klikkbar lenke, åpnet i nettleser, landet på en
|
||
fungerende `/verify`-side, sesjon opprettet — brukeren bekreftet innlogget
|
||
status. Første gang en hel bruker-vendt flyt er bevist ende-til-ende i
|
||
produksjon, ikke bare API-et isolert.
|
||
**Merk for neste økt:** `deploy/Caddyfile`-endringen ligger uncommitted i
|
||
det SEPARATE `/opt/teeoff`-repoet, ikke i `teecup`-repoet — lett å glemme
|
||
siden denne økten ellers kun har jobbet i `/opt/teecup`.
|
||
- **Dashboard-skjerm LIVE (2026-07-18):** andre V0-skjerm — organisasjon-
|
||
bytter/-opprettelse + turneringsliste (`/dashboard`), samme mønster som
|
||
login-runden. **Reell integrasjonsfelle unngått:** V0s eksport denne gangen
|
||
var en FULL re-eksport av hele prosjektet (inkl. `login-form.tsx`,
|
||
`next.config.mjs`, `package.json`), ikke bare de nye filene — en naiv
|
||
utpakking ville stille reversert `rewrites()`-proxyen, `output:
|
||
"standalone"`, den ekte fetch-kablingen i login-skjemaet, og alle
|
||
V0-uavhengige opprydninger fra forrige runde. Løst ved å pakke ut til et
|
||
scratch-område FØRST, diffe mot live-treet fil for fil, og kun ta inn det
|
||
som faktisk var nytt (`dashboard.tsx`, `tournament-card.tsx`,
|
||
`tournament-status-badge.tsx`, `wordmark.tsx`, `badge.tsx`,
|
||
`dropdown-menu.tsx`, `app/dashboard/page.tsx`) — `next.config.mjs`,
|
||
`package.json`, `globals.css`, Docker-filene ble bevisst IKKE overskrevet.
|
||
`login-form.tsx` fikk en kirurgisk patch (kun Wordmark flyttet til egen
|
||
fil, som V0 selv hadde gjort — all egen fetch-/feilhåndteringslogikk
|
||
urørt).
|
||
**`dashboard.tsx` sitt datalag skrevet om fra bunnen** (V0 leverte kun
|
||
mock `useState`): henter `/auth/me` for organisasjonsmedlemskap,
|
||
`/orgs/{id}/tournaments` per valgt org, `POST /orgs`/`POST
|
||
/orgs/{id}/tournaments` for opprettelse, `POST /auth/logout` for utlogging
|
||
— presentasjonskomponentene (kort, bytter, tomme tilstander) beholdt
|
||
uendret fra V0. `/verify`-siden oppdatert til å sende brukeren videre til
|
||
`/dashboard` etter vellykket innlogging (fantes ingen dit å gå før nå).
|
||
**Verifisert:** ekte typesjekket build, redeploy av kun `teecup_frontend`
|
||
(ingen Caddy-endring nødvendig denne gangen — ADR-016s mønster holder),
|
||
`teecup.teeoff.no/dashboard` → 200, `teeoff.no` upåvirket.
|
||
**Skrive-flyten bekreftet med EKTE data samme dag (brukeren testet selv,
|
||
ikke meg):** organisasjon "Tjøme Gents" og turnering "De Gamle er Eldst"
|
||
opprettet via UI-et mot den ekte `teecup_db` — statusmerket viste riktig
|
||
"Utkast", "Ingen datoer satt" håndtert korrekt (ingen krasj på manglende
|
||
dato), ett-org-visningen viste riktig uten unødvendig bytter-UI. Første
|
||
gang en HEL skrive-flyt (ikke bare lesing) er bevist ende-til-ende fra
|
||
frontend mot ekte produksjonsdata.
|
||
- **Lag/roster-skjerm LIVE (2026-07-18):** tredje V0-skjerm
|
||
(`/tournaments/[id]`), samme re-eksport-mønster som dashboard-runden —
|
||
diffet mot live-treet, tok kun inn `tournament-detail.tsx` og et
|
||
`Link`-basert `tournament-card.tsx` (navigasjon fra dashbordet).
|
||
**URL-design bevisst avvikende fra V0s forslag:** V0s genererte side leste
|
||
aldri `params.id` og hadde ingen organization_id i det hele tatt — holdt
|
||
derfor V0s flate `/tournaments/[id]`-struktur (i stedet for en nøstet
|
||
`/orgs/[orgId]/tournaments/[id]`, som ville krevd manuell ombygging ved
|
||
HVER fremtidig V0-reeksport) og la `org`+`name` til som søkeparametre i
|
||
`tournament-card.tsx` sin lenke — API-et krever organization_id på alle
|
||
team-/roster-kall (RLS).
|
||
**Reelt hull funnet FØR integrering, ikke etter:** V0-skjermen bygger inn
|
||
"fjern spiller"/"gjør til kaptein"-handlinger, men backend hadde KUN
|
||
GET/POST på `team_roster` — ingen DELETE eller PATCH. Spurte bruker
|
||
eksplisitt (samme mønster som andre scope-avklaringer denne økten) — svar:
|
||
bygg de to endepunktene nå. Lagt til i `app/routers/tournaments.py`:
|
||
`PATCH .../roster/{roster_id}` (bevisst enkel — setter/fjerner
|
||
`is_captain` på NØYAKTIG denne raden, håndhever IKKE "kun én kaptein per
|
||
lag", siden kaptein fortsatt bare er et merke, ikke en egen
|
||
autorisasjonsrolle) og `DELETE .../roster/{roster_id}` (204, idempotent
|
||
`NOT_FOUND` ved dobbel sletting — ikke krasj).
|
||
**`tournament-detail.tsx` sitt datalag skrevet om fra V0s mock:** henter
|
||
lag + roster (roster-radene bærer allerede `display_name`/
|
||
`handicap_index_snapshot` fra APIet, så V0s separate `poolById`-oppslag
|
||
ble fjernet som overflødig) og organisasjonens spillerpool
|
||
(`GET /orgs/{id}/players`, brukt til type-ahead ved "legg til spiller").
|
||
Alle fem mutasjonene (opprett lag, legg til eksisterende spiller, opprett
|
||
ny spiller inline + rostre, endre kaptein, fjern fra roster) kablet mot
|
||
ekte endepunkter — presentasjonskomponentene (kort, type-ahead,
|
||
fargevelger, bekreft-fjerning) beholdt uendret fra V0.
|
||
**Verifisert:** de to nye endepunktene testet mot en fersk
|
||
`teecup_scratch` (PATCH setter kaptein + riktig `NOT_FOUND` på ugyldig id,
|
||
DELETE gir 204 + idempotent `NOT_FOUND` ved gjentak, `test_isolation.sql`
|
||
fortsatt 12/12), ekte typesjekket frontend-build (5 ruter), redeploy av
|
||
BEGGE containere (backend-endepunktene er nye), `teecup.teeoff.no/dashboard`
|
||
→ 200, `teeoff.no` upåvirket. Selve skrive-flyten på `/tournaments/[id]`
|
||
(opprett lag/roster) ikke testet med ekte data i denne runden — venter på
|
||
brukeren, samme mønster som dashboard-rundens skrive-test.
|
||
- **ADR-017 + migrasjon 007 (2026-07-18):** brukeren reiste selvregistrering
|
||
rett etter at roster-skrive-flyten var bekreftet. Full ADR skrevet
|
||
(5 beslutninger: offentlig påmelding uten innlogging, e-post som
|
||
sammenkoblingsnøkkel mot forhåndsopprettede spillere, egen
|
||
`tournament_registration`-tabell atskilt fra `team_roster` med
|
||
konfigurerbar godkjenning/kapasitet/venteliste, utvidet spillerprofil +
|
||
obligatorisk samtykke, og `public_tournament_org()`). Migrasjon
|
||
`007_registration_and_player_fields.sql` skrevet og scratch-verifisert
|
||
(001→007 kjører rent, `test_isolation.sql` fortsatt 12/12).
|
||
**Reelt arkitekturproblem løst underveis, ikke bare skjema:** et
|
||
offentlig (uautentisert) påmeldingskall kjenner en turnering-id, men
|
||
ingen org-kontekst — og uten `app.current_org` slipper RLS ingen rader
|
||
gjennom, heller ikke oppslaget for å FINNE riktig org. Løst med en snever
|
||
`SECURITY DEFINER`-funksjon (`public_tournament_org`) som KUN eksponerer
|
||
uuid→uuid-koblingen. **Verifisert presist, ikke bare "kjørte uten feil":**
|
||
kalt funksjonen som `teecup_app`-rollen med ingen `app.current_org` satt
|
||
— ga korrekt org-id for en kjent turnering, `NULL` (ikke feil) for en
|
||
ukjent — OG et RÅTT `SELECT` på `tournament` på SAMME tilkobling/rolle ga
|
||
fortsatt 0 rader, som beviser RLS ikke er brutt generelt, bare dette ene
|
||
smale unntaket eksisterer.
|
||
**Kjørt mot ekte `teecup_db` 2026-07-18** (bruker bekreftet eksplisitt i
|
||
samme økt): migrasjonen kjørte rent, `test_isolation.sql` fortsatt 12/12.
|
||
- **Registrerings-API LIVE (2026-07-18), samme dag:** ny
|
||
`app/routers/registration.py` — `GET /public/tournaments/{id}` og
|
||
`POST /public/tournaments/{id}/register`, begge UTEN `get_current_user`
|
||
eller `get_authorized_org` (helt uautentisert, egen `/public`-prefiks,
|
||
bevisst atskilt fra `/orgs/...` i koden). Bruker `public_tournament_org()`
|
||
(migrasjon 007) til å slå opp org-kontekst FØR RLS kan håndheve noe.
|
||
`app/routers/players.py` utvidet med alle sju nye ADR-017-feltene
|
||
(`mobile`/`email`/`birth_date`/`nickname`/`country`/`club`/
|
||
`club_member_number`). `frontend/next.config.mjs` sin `rewrites()`
|
||
utvidet med `/public/*` (ADR-016s konsekvens: enhver ny API-prefiks MÅ
|
||
inn her).
|
||
**Ny brukers-oppdaget notat fanget FØR bygging, ikke etter:** brukeren
|
||
krevde eksplisitt at synlighet (offentlig/kun org/kun turnering-
|
||
deltakere) må være et VALG for fremtidige landingssider — notert grundig
|
||
i `FEATURE_BACKLOG.md` (koblet til samme åpne spørsmål for "Banter
|
||
Board"-feeden) FØR dette registrerings-API-et ble bygget, akkurat for at
|
||
det ikke skal gå i glemmeboken til landingsside-runden.
|
||
**Verifisert grundig mot fersk `teecup_scratch`** (001→007,
|
||
`test_isolation.sql` 12/12): hele registreringsløpet testet reelt —
|
||
samtykke-avvisning (400), duplikat-avvisning (409 `DUPLICATE`),
|
||
e-post-matching mot en organisator-forhåndsopprettet spiller (BEKREFTET:
|
||
ingen duplikatrad, mobil fylt inn via `COALESCE`, `display_name` IKKE
|
||
overskrevet), kapasitet+`waitlist`-policy (→ `waitlisted`),
|
||
kapasitet+`closed`-policy (→ 409 `LIMIT_REACHED`), `registration_
|
||
requires_approval` (→ `pending`), utløpt frist (→ 409
|
||
`REGISTRATION_CLOSED`), og `confirmed_count` i `GET`-responsen talt
|
||
riktig (kun `confirmed`+`pending`, ikke `waitlisted`/avviste).
|
||
**Rullet ut live:** begge containere redeployet, `teecup.teeoff.no/
|
||
dashboard` og `/health` fortsatt 200, `teeoff.no` upåvirket, det
|
||
offentlige endepunktet bekreftet nåbart over ekte https (ukjent
|
||
turnering-id ga korrekt `404`/`NOT_FOUND`, ikke-destruktiv sjekk — selve
|
||
påmeldingsflyten med ekte data ikke testet mot prod i denne runden).
|
||
**E-post-basert kontosammenkobling LIVE, samme dag:** ny migrasjon
|
||
`008_link_player_by_email.sql` — `link_player_by_email(user_id, email)`,
|
||
samme `SECURITY DEFINER`-mønster som `public_tournament_org()` (007),
|
||
denne gangen for en tverr-org UPDATE i stedet for et lese-oppslag.
|
||
`verify_magic_link` (`app/routers/auth.py`) kaller den på HVER
|
||
innlogging (idempotent — funksjonens `WHERE user_id IS NULL` gjør
|
||
gjentatte kall til en no-op), ikke bare ved førstegangsopprettelse.
|
||
**Verifisert presist:** to separate org-er, hver med sin egen
|
||
organisator-opprettede "Kari"-rad (samme e-post, ulik store/små
|
||
bokstaver for å teste case-insensitivitet også) — ved Karis FØRSTE
|
||
innlogging ble BEGGE radene koblet til kontoen hennes, i to org-er hun
|
||
aldri har vært medlem av. Eksplisitt bekreftet: `organization_membership`
|
||
har NULL rader for henne etterpå — ren identitetskobling, ingen
|
||
privilegie-eskalering (å ha `player.user_id` satt gir ingen ny tilgang
|
||
gjennom `get_authorized_org`, som fortsatt krever ekte org-medlemskap
|
||
uavhengig av dette). Andre innlogging idempotent, ingen feil.
|
||
`test_isolation.sql` fortsatt 12/12. **Kjørt mot ekte `teecup_db`
|
||
2026-07-18**, bruker bekreftet eksplisitt, backend redeployet, live
|
||
sjekker OK, `teeoff.no` upåvirket.
|
||
**ADR-017s backend er dermed komplett** (registrering + kontokobling).
|
||
Gjenstående: frontend-påmeldingsskjema og landingssider — egen, senere
|
||
ADR-runde (se `FEATURE_BACKLOG.md`).
|
||
- **ADR-018 + migrasjon 009: landingssider, backend LIVE (2026-07-18),
|
||
samme dag:** trenivås synlighet (`tournament.visibility`:
|
||
`public`/`org`/`participants`, default `org` — trygg standard) +
|
||
`organization.public_profile`. **Reell presisering funnet underveis, ikke
|
||
antatt på forhånd:** RLS (`org_isolation`) beskytter kun TENANT-grenser
|
||
(org A ser aldri org B), IKKE innholds-synlighet innenfor riktig
|
||
org-kontekst — det eksisterende `GET /public/tournaments/{id}` (ADR-017)
|
||
leste allerede fullt innhold uten synlighetssjekk, fordi RLS er fornøyd
|
||
så snart org-konteksten er satt, uansett hvem som spør. `visibility`
|
||
håndheves derfor eksplisitt i `app/routers/registration.py`, på BÅDE
|
||
lesing og registrering (ADR-018 Beslutning D: kan du ikke se turneringen,
|
||
kan du heller ikke melde deg på den — bekreftet med bruker FØR bygging).
|
||
Ny `get_current_user_optional` i `app/auth.py` (som `get_current_user`,
|
||
men returnerer `None` i stedet for 401 — offentlige endepunkter skal
|
||
fungere for anonyme lesere også). Ny "deltaker"-autorisasjonsvei: en
|
||
innlogget bruker med `player.user_id` koblet (ADR-017) OG en
|
||
`tournament_registration`- eller `team_roster`-rad for NØYAKTIG den
|
||
turneringen får se `participants`-synlige turneringer, uansett
|
||
org-medlemskap.
|
||
**Ekte migrasjonsfeil funnet OG rettet UNDER scratch-verifisering, ikke
|
||
antatt riktig:** migrasjonen feilet først ("column slug already exists")
|
||
— `organization.slug` har ligget i skjemaet siden migrasjon 001
|
||
("f.eks. subdomene/URL-vennlig", allerede med en plain `UNIQUE`), noe jeg
|
||
hadde oversett fullstendig og forsøkt å legge til på nytt. Rettet ved å
|
||
fjerne den doble `ADD COLUMN` + den overflødige partielle unik-indeksen
|
||
(001 sin plain `UNIQUE` dekker "unik når satt" allerede, siden Postgres
|
||
behandler NULL som distinkt), beholde kun de nye `CHECK`-constraintene.
|
||
Kjørte rent på ny etter fiksen. **Lærdom:** grep alltid eksisterende
|
||
skjema for feltnavn FØR en ny migrasjon skrives, ikke bare stol på
|
||
hukommelsen om hva som "sikkert" ikke finnes fra før.
|
||
Ny tabell `tournament_sponsor` (navn+lenke aktivt, `logo_key` inert til
|
||
MinIO-runden — samme med `tournament.hero_image_key`). Tredje
|
||
`SECURITY DEFINER`-bro i prosjektet: `public_org_by_slug()` (etter
|
||
`public_tournament_org` 007, `link_player_by_email` 008) — returnerer
|
||
`NULL` for BÅDE "finnes ikke" og "finnes, men er privat", samme
|
||
anti-enumerering som magic-link.
|
||
**Fylte også et implisitt hull oppdaget underveis:** ADR-en beskrev
|
||
hvordan synlighet skulle håndheves, men ingen tidligere runde hadde bygget
|
||
noen vei for organisator til faktisk å SETTE disse feltene. Lagt til:
|
||
`PATCH /orgs/{id}/tournaments/{id}` (visibility/description/
|
||
registrerings-innstillinger, ekte PATCH-semantikk via Pydantic sin
|
||
`exclude_unset` — et utelatt felt nullstilles IKKE), `PATCH /orgs/{id}`
|
||
(slug/public_profile), full sponsor-CRUD. `_fetch_sessions()` trukket ut
|
||
som delt hjelpefunksjon i `tournaments.py` (delt mellom den innloggede
|
||
og den nye offentlige `GET /public/tournaments/{id}/sessions` — blind
|
||
draw-hemmelighold, ADR-013, arves automatisk, ikke reimplementert).
|
||
**Verifisert grundig mot fersk `teecup_scratch`** (001→009,
|
||
`test_isolation.sql` 12/12): hele synlighetsmatrisen testet med ekte
|
||
HTTP-kall — anonym avvist på `org`-synlig turnering (både lesing OG
|
||
registrering), `PATCH` til `public` + beskrivelse + sponsor fungerte,
|
||
anonym lesing fungerte deretter, `participants`-synlighet bekreftet
|
||
reell chicken-and-egg-konsekvens av Beslutning D (ingen kan selv-
|
||
registrere seg til en `participants`-synlig turnering, kun organisator
|
||
kan legge til direkte — korrekt, ikke en bug), en organisator-rostret
|
||
spiller som logget inn fikk tilgang, en tilfeldig innlogget FREMMED
|
||
(ikke deltaker) ble fortsatt avvist, org-landingsside viste KUN
|
||
`public`-synlige turneringer, `CHECK`-constraint (`public_profile`
|
||
krever `slug`) avvist korrekt, ugyldig slug-format avvist av Pydantic,
|
||
sponsor-sletting fungerte.
|
||
**Kjørt mot ekte `teecup_db` 2026-07-18**, bruker bekreftet eksplisitt,
|
||
backend redeployet, live sjekker OK, `teeoff.no` upåvirket. Ingen
|
||
frontend-endring nødvendig for selve API-tilgangen (det brede
|
||
`/public/:path*`-mønsteret fra ADR-016 dekker allerede `/public/orgs/*`).
|
||
**Gjenstår:** selve landingsside-SKJERMENE i frontend (V0), og
|
||
MinIO/bildeopplasting — bevisst utsatt, egen runde.
|
||
- **Offentlig turnering-landingsside LIVE (2026-07-18), samme dag:** fjerde
|
||
V0-skjerm (`components/public-tournament.tsx`, ny rute `/t/[id]`) —
|
||
banner (bevisst permanent farge-/gradient-utseende, ikke en "bilde
|
||
kommer"-plassholder), presenterende tekst, status (datoer + "X av Y
|
||
plasser"), program, sponsorer (navn+lenke), og et påmeldingsskjema med
|
||
navn+e-post synlig først og resten bak en "flere detaljer"-utvidelse —
|
||
tre distinkte bekreftelsestilstander (bekreftet/venteliste/godkjenning
|
||
venter), ikke én generisk "takk".
|
||
**Ingen ny reeksport-kollisjon denne gangen** — kun étt genuint nytt
|
||
filnavn (`public-tournament.tsx`), resten var kjent V0-revert av
|
||
allerede-tilpassede filer, samme mønster som før.
|
||
**V0 opprettet komponenten, men ingen rute** — la selv til `app/t/[id]/
|
||
page.tsx` (bevisst en FLAT `/t/[id]`-sti, ikke nøstet under `/orgs/...`
|
||
som den innloggede turnering-detalj-siden, siden det offentlige API-et
|
||
kun trenger turnering-id, ikke org-id).
|
||
**Datalaget skrevet om fra V0s mock:** ekte `fetch` mot
|
||
`GET /public/tournaments/{id}` + `/sessions`, ekte `POST .../register`.
|
||
Håndterer 403 (`NOT_VISIBLE`, ADR-018) og 404 med en egen tilgang-avvist-
|
||
tilstand V0 ikke hadde bedt om (fantes ikke i prompten, men trengs for at
|
||
siden faktisk skal fungere for `org`/`participants`-synlige turneringer).
|
||
Mappet `status:"waitlisted"` fra API-et til komponentens `"waitlist"`, og
|
||
skjemaets norske kjønnsvalg ("kvinne"/"mann"/"annet") til API-ets
|
||
`^[mfx]$`-mønster — to reelle navnekollisjoner mellom V0s UI-språk og
|
||
API-kontrakten, ikke bare et rett-frem felt-for-felt-uttrekk.
|
||
**Reell driftsfeil funnet OG rettet under scratch-test, ikke i
|
||
produksjon:** `pnpm install` (uten `--ignore-scripts`) i dev-server-
|
||
testoppsettet feilet stille med tom logg og exit 1 — corepack hadde
|
||
hentet en NY pnpm-versjon (11.13.1 → 11.14.0) siden sist, som gjør
|
||
"ignored builds"-varselet til en hard feil i stedet for bare en
|
||
advarsel. Rettet ved å bruke samme `--ignore-scripts`-flagg som
|
||
`frontend/Dockerfile` allerede bruker (upåvirket av denne — bekreftet
|
||
ved at selve prod-buildet fortsatt gikk rent). **Lærdom:** `Dockerfile`
|
||
sin pinning av *kode* er ikke det samme som å pinne *verktøyene rundt*
|
||
(corepack henter alltid siste pnpm) — verdt å huske neste gang et
|
||
scratch-dev-server-oppsett plutselig feiler uten åpenbar grunn.
|
||
**Verifisert grundig mot fersk `teecup_scratch` + en ekte kjørende
|
||
frontend-dev-server** (ikke bare `next build`): ekte turnering med
|
||
beskrivelse, kapasitet, sponsor og økt opprettet via API-et, hentet
|
||
gjennom frontend-proxyen (ikke direkte mot backend) og bekreftet
|
||
byte-for-byte riktig — inkludert en ekte `POST`-registrering som økte
|
||
`confirmed_count` fra 0 til 1.
|
||
**Rullet ut live**, kun `teecup_frontend` (ingen backend-endring denne
|
||
runden), `teeoff.no` upåvirket.
|
||
**Gjenstår:** org-landingssiden (egen V0-prompt), Open Graph-metadata for
|
||
deling, MinIO/bilder.
|
||
- **Offentlig klubb-landingsside LIVE (2026-07-18), samme dag:** femte og
|
||
siste V0-skjerm i ADR-018 (`components/public-club.tsx`, ny rute
|
||
`/clubs/[slug]`). Samme banner-språk som turnering-siden, liste over
|
||
klubbens turneringer (gjenbrukte eksisterende `TournamentCard`), egen tom-
|
||
tilstand.
|
||
**Reell delt-komponent-kollisjon løst, ikke duplisert bort:**
|
||
`TournamentCard` var bygget for KUN den innloggede konteksten (krevde
|
||
`orgId`, lenket til `/tournaments/{id}?org=...`). I stedet for en egen
|
||
kopi av kortet for den offentlige siden, gjort `orgId` valgfri —
|
||
satt (dashbordet) gir innlogget lenke, utelatt (klubbsiden) gir
|
||
`/t/{id}` i stedet. Samme kort, to kontekster, ingen duplisering.
|
||
Bekreftet dashbordets egen bruk uendret/upåvirket etterpå.
|
||
**V0 opprettet denne gangen selv en rute** (`app/clubs/[id]/page.tsx`,
|
||
uten noen props sendt inn i det hele tatt) — men navnga parameteren
|
||
`[id]` selv om den faktisk er en SLUG (`public_org_by_slug()`, ADR-018).
|
||
Skrev selv en ny, riktig `app/clubs/[slug]/page.tsx` i stedet for å bruke
|
||
V0s (feilnavngitte og prop-løse) versjon.
|
||
**Verifisert mot fersk `teecup_scratch` + ekte kjørende frontend-dev-
|
||
server:** org med slug+`public_profile`, én turnering satt `public`, én
|
||
latt stå på default `org` — klubbsiden viste GJENNOM frontend-proxyen
|
||
kun den ene offentlige turneringen, den org-private var korrekt
|
||
usynlig (samme filtermønster som API-et selv, bekreftet fra
|
||
klientsiden også). Ukjent slug ga korrekt 404.
|
||
**Rullet ut live**, kun `teecup_frontend`, `teeoff.no` upåvirket.
|
||
**ADR-018s planlagte skjermer er dermed komplette.** Gjenstår: Open
|
||
Graph-metadata for deling, MinIO/bilder — begge bevisst egne, senere
|
||
runder.
|
||
- **Open Graph-metadata LIVE (2026-07-18), samme dag — ADR-018 dermed
|
||
helt ferdig:** `generateMetadata()` lagt til på `/t/[id]` og
|
||
`/clubs/[slug]` (ekte tittel/beskrivelse fra API-et, `og:site_name`,
|
||
trygg fallback-tittel ved ukjent id/slug — feil her skal ALDRI hindre
|
||
selve siden i å laste).
|
||
**Reell driftsfeil funnet FØR den nådde produksjon, ikke etter:**
|
||
`generateMetadata()` kjører server-side ved REQUEST-tid, ikke i
|
||
nettleseren — går derfor IKKE gjennom `next.config.mjs` sin
|
||
`rewrites()` (som kun gjelder nettleser-trafikk inn til Next.js-
|
||
serveren). Måtte derfor lese `TEECUP_API_ORIGIN` direkte, men den
|
||
variabelen fantes KUN i `Dockerfile` sitt builder-steg — `ENV` satt i
|
||
ett `FROM`-steg arves ikke til et senere. Rettet ved å sette samme
|
||
`ARG`/`ENV` på nytt i runner-steget også.
|
||
**Verifisert presist at fiksen faktisk virker, ikke bare at bygget gikk
|
||
gjennom:** bygget det EKTE produksjonsimaget (ikke dev-server) pekt mot
|
||
en scratch-backend med en ekte offentlig turnering+org, hentet den
|
||
faktiske server-rendrede HTML-en og bekreftet ekte `<title>`/`og:title`/
|
||
`og:description` — ikke bare at TypeScript kompilerte. Ukjent
|
||
turnering-id ga korrekt trygg fallback-tittel.
|
||
**Bevisst utenfor omfang:** `og:image` — ingen ekte bilde finnes ennå
|
||
(MinIO-runden). La til `metadataBase` i `app/layout.tsx` nå likevel, som
|
||
forarbeid slik at et fremtidig relativt bilde-URL løses riktig uten en
|
||
egen fiks da.
|
||
**Rullet ut live**, kun `teecup_frontend`, `teeoff.no` upåvirket.
|
||
- **MinIO-runden LIVE (2026-07-18), samme dag — ADR-018 dermed HELT
|
||
ferdig, ingenting utsatt igjen:** ny `teecup-minio`-tjeneste (persistent
|
||
volum, genererte credentials i `.env`), backend-endepunkter for ekte
|
||
bildeopplasting til `tournament.hero_image_key`/`tournament_sponsor.
|
||
logo_key` (som lå inerte siden migrasjon 009), offentlige API-svar bygger
|
||
nå fulle URL-er, `og:image` koblet på i `/t/[id]` sin `generateMetadata`.
|
||
**Sent, men viktig presisert krav underveis:** brukeren avbrøt en
|
||
verktøyskall midtveis for å presisere at ALLE bilder skal konverteres
|
||
til AVIF for plassbesparelse. Snudde HELE opplastingsarkitekturen på
|
||
dette: droppet den opprinnelige planen om presignerte URL-er (nettleser
|
||
laster opp DIREKTE til MinIO) til fordel for ekte multipart-opplasting
|
||
GJENNOM API-et, som konverterer til AVIF (Pillow + pillow-avif-plugin)
|
||
FØR lagring. Dette forenklet arkitekturen betydelig: kun ÉN MinIO-klient
|
||
trengs nå (før: to separate klient-oppsett for henholdsvis internt
|
||
admin-arbeid og offentlig presignering), og Caddy sin nye rute trenger
|
||
ikke lenger bevare Host-headeren presist (relevant kun for SigV4-
|
||
signaturverifisering av presignerte URL-er, ikke for anonym public-read).
|
||
**To reelle feil funnet UNDER scratch-verifisering, aldri i produksjon:**
|
||
(1) `pillow-avif-plugin` krever ingen ekstra systempakker i
|
||
`python:3.12-slim` -- verifisert med et frittstående encode/decode-
|
||
rundtrip FØR det ble tatt i bruk i selve API-et. (2) MinIO validerer
|
||
`Host`-headeren STRENGT og avviser understrek som ugyldig vertsnavn --
|
||
et tjenestenavn med understrek (`teecup_minio`, konsistent med
|
||
`teecup_api`/`teecup_frontend`) feilet umiddelbart ved oppstart
|
||
("Invalid Request (invalid hostname)"). Bekreftet presist ved å teste
|
||
samme oppsett med bindestrek i stedet (`teecup-minio`) -- fungerte
|
||
umiddelbart. Alle referanser rettet til bindestrek FØR noe ble forsøkt
|
||
mot ekte infrastruktur.
|
||
**Caddy:** ny `/teecup-media/*`-rute på det EKSISTERENDE
|
||
`teecup.teeoff.no`-blokket (IKKE et nytt subdomene -- en tidlig sjekk
|
||
avdekket at `media.teecup.teeoff.no` fantes som en wildcard DNS-post,
|
||
men pekte til en IPv6-adresse denne serveren ikke har i det hele tatt;
|
||
path-prefiks på et allerede fungerende domene unngikk hele den DNS-
|
||
avhengigheten). Samme stale-inode-oppførsel som alle tidligere Caddy-
|
||
runder -- full `docker restart teeoff_caddy`, brukeren bekreftet
|
||
eksplisitt. `teeoff.no` upåvirket.
|
||
**Verifisert grundig, i flere lag:** frittstående AVIF-encode/decode-
|
||
test, full scratch-kjede (fersk Postgres + en ISOLERT scratch-MinIO-
|
||
container) med et ekte opplastet bilde -- bekreftet konvertert til
|
||
gyldig AVIF (800×400, 406 bytes for et helfarget testbilde), bekreftet
|
||
lagret med riktig nøkkel, bekreftet lesbart ANONYMT direkte mot MinIO
|
||
(uten Caddy, isolerer bucket-policyen), bekreftet `hero_image_url`/
|
||
`logo_url` bygget riktig i det offentlige API-svaret. Alle tre
|
||
valideringsveier testet (ugyldig content-type, korrupt bildeinnhold,
|
||
for stor fil >8MB) -- alle ga korrekt `VALIDATION_FAILED`. Etter
|
||
Caddy-omstart: et ekte anonymt kall mot `/teecup-media/...` på
|
||
produksjonsdomenet ga en ekte MinIO S3-XML-feilrespons (`NoSuchKey` for
|
||
en scratch-nøkkel som naturligvis ikke finnes i prod) -- beviser ruten
|
||
treffer MinIO selv, ikke frontend sin egen 404-side.
|
||
`test_isolation.sql` fortsatt 12/12 gjennom hele runden.
|
||
**Bevisst utenfor omfang:** ingen faktisk opplasting av et EKTE bilde
|
||
til en EKTE, live turnering i denne runden (ville krevd å skrive
|
||
test-data i brukerens ekte konto uten å bli spurt) -- tilbudt, ikke
|
||
utført. Selve dra-og-slipp-opplastingsskjermen i frontend (V0) er
|
||
fortsatt ikke bygget, som avtalt fra starten av runden.
|
||
|
||
- **Program-skjerm (økter/tidsplan), bygget og SCRATCH-verifisert, IKKE
|
||
live ennå (2026-07-18):** sjette V0-skjerm, første i "bygg i rekkefølgen
|
||
ting brukes"-serien (etter lag/roster: program → blind draw → scorekort →
|
||
leaderboard). `components/tournament-program.tsx`, ny rute
|
||
`/tournaments/[id]/program`. Tidslinje over økter + opprett-skjema
|
||
(format, hullomfang, scoringsmodus, poeng, klokkeslett+intervall,
|
||
starthull, kollapsbar "avansert"-seksjon med ADR-014s fire brytere).
|
||
**Reelt blokkerende hull funnet FØR integrering:** `SessionCreate.
|
||
course_id` er påkrevd, men INGEN endepunkt kunne noensinne produsere en —
|
||
ADR-004s teeoff-integrasjon er fortsatt bare vedtatt (ingen utgående
|
||
HTTP-klient finnes). Spurte bruker eksplisitt — svar: bygg enkel
|
||
course-CRUD nå. Ny `app/routers/courses.py` (`GET`/`POST /orgs/{id}/
|
||
courses`, kun `source='custom'`). Program-skjemaet fikk et
|
||
type-ahead-felt for bane (samme mønster som spiller-type-ahead i
|
||
roster-skjermen).
|
||
**Reell korrekthetsfeil rettet FØR integrering:** 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 -- ren `"scramble"` avvises med 400. Rettet i
|
||
frontend-mappingen til to segment-knapper.
|
||
**`allowance_override`-JSON-formen verifisert eksakt** mot
|
||
`app/handicap.py` sin `parse_allowance_config`/`_strategy_from_json`
|
||
(`{type: "combined"|"per_player", percentage: 0..1}`, ikke en flat
|
||
prosent) -- frontend velger riktig `type` ut fra om formatet er
|
||
side-enhet eller spiller-enhet, konverterer 0–100-skjemafelt til 0–1.
|
||
**Scratch-infrastruktur denne runden, bevisst forskjellig fra tidligere:**
|
||
brukte en ISOLERT `teecup_app_scratch`-rolle (`GRANT teecup_app TO
|
||
teecup_app_scratch`) i stedet for den ekte `teecup_app`-rollen -- den er
|
||
nå cluster-global og produksjonskritisk (delt Postgres-instans med
|
||
`teecup_db`), så et eldre plandokuments "drop teecup_app-rolle"-
|
||
opprydning (skrevet FØR go-live) er utdatert og ble bevisst IKKE fulgt.
|
||
Egen isolert scratch-MinIO-container også (app-oppstart krever en
|
||
nåbar MinIO for `ensure_bucket()`).
|
||
**Verifisert:** courses opprettet+listet, kryss-org-isolasjon bekreftet,
|
||
økt med klokkeslett, økt med `scramble_4`+full `allowance_override`-
|
||
rundtur, gammel `"scramble"`-verdi avvist (400), `test_isolation.sql`
|
||
12/12, ekte typesjekket PRODUKSJONSBUILD (samme `Dockerfile` som faktisk
|
||
deployes) kjørt og bekreftet.
|
||
**Diffet V0-eksporten mot live-treet FØR noe ble tatt inn** (samme mønster
|
||
som alle tidligere runder): kun tre reelt nye filer, resten forventede
|
||
full-reverts, ikke rørt. Fjernet V0s dev-only forhåndsvisnings-toggle;
|
||
lagt til fanerad ("Lag og spillere" / "Program") i BEGGE skjermene siden
|
||
V0 ikke visste om den andre når den ble generert i egen prompt.
|
||
**Rullet ut live 2026-07-18**, bruker bekreftet eksplisitt: `docker
|
||
compose up -d --build teecup_api teecup_frontend` (kun disse to, `teecup-
|
||
minio` urørt). Verifisert: begge containere boot-et rent (`Application
|
||
startup complete`, Next.js `Ready`), `teecup.teeoff.no/health` og
|
||
`/dashboard` → 200, `teeoff.no` upåvirket (200).
|
||
- **ADR-004 (teeoff-banedata) kartlagt, IKKE bygget (2026-07-18):** brukeren
|
||
påpekte rett etter program-skjerm-rullingen at ADR-004s teeoff-
|
||
integrasjon fortsatt bare er vedtatt, ikke bygget (kun `source='custom'`
|
||
finnes). Kartlagt `/opt/teeoff/backend/main.py` (annet repo, kun lest):
|
||
`GET /api/facilities/{slug}` (offentlig, INGEN auth/API-nøkkel) returnerer
|
||
allerede alt teecup trenger — `courses[].holes[]` (`par`, `hcp_index`
|
||
= stroke index), `courses[].tees[]` (`name`, `cr_men`/`slope_men`,
|
||
`cr_women`/`slope_women` — kjønnsdelt WHS-rating). CORS-lista på teeoff-
|
||
siden ekskluderer teecups origin, men er IRRELEVANT for et server-til-
|
||
server-kall (kun nettleser-fetch rammes av CORS). Ingen dokumentasjon
|
||
(README/OpenAPI) finnes for dette API-et i teeoff-repoet — kartleggingen
|
||
over er basert på å lese koden direkte. Stabil identifikator: `facilities.
|
||
slug` (f.eks. `borregaard-golfklubb`) — selve banen har kun en intern
|
||
serial-id, ingen egen slug, så `course.external_course_ref` må bære
|
||
facility-slug + bane-id sammen.
|
||
**Oppdatering samme dag: brukeren ba meg ta fatt på dette NÅ** (bevisst
|
||
sidesprang fra "bygg i rekkefølgen ting brukes"-planen — blind draw-
|
||
skjermen er fortsatt neste steg i den planen ETTERPÅ, ikke droppet).
|
||
Design besluttet og skrevet som **ADR-019** (se ARCHITECTURE_DECISIONS.md):
|
||
import (kopi) ved organisators eksplisitte valg, ikke live oppslag ved
|
||
hver bruk — fryser data på importtidspunktet, samme reproduserbarhets-
|
||
prinsipp som handicap-snapshotten (ADR-007). Server-til-server-kall
|
||
(`teecup_api` → `http://teeoff_api:8000`, internt Docker-nettverk, ingen
|
||
auth trengs). Ny migrasjon `010` (unik `external_course_ref` per org,
|
||
hindrer dupliserte importer). Se ADR-019 for alle fem delbeslutningene.
|
||
**Bygget, scratch-verifisert MOT EKTE `teeoff_api`** (ikke en simulert
|
||
respons — ekte HTTP-kall til den kjørende produksjonscontaineren, kun
|
||
lesing): søkte opp "Borregaard" i teeoff sine 174 publiserte anlegg,
|
||
hentet Borregaard Golfklubb sin hovedbane (18 hull, 4 tee-farger ×
|
||
kjønn), importerte den til en scratch-org — alle 18 hull med riktig
|
||
par/stroke-index (1-18, unik), alle 8 tee/tee_rating-rader med riktig
|
||
full_18 course/slope-rating verifisert direkte i databasen, opprettet
|
||
deretter en ekte økt med den importerte banen som `course_id` (beviser
|
||
hele veien til handicap-motoren fungerer, ikke bare selve importen).
|
||
Reimport av samme bane korrekt avvist (409 DUPLICATE, migrasjon 010 sin
|
||
indeks). Kryss-org-isolasjon bekreftet, ukjent teeoff-slug ga korrekt 404,
|
||
`test_isolation.sql` 12/12. Ekte typesjekket produksjonsbuild av
|
||
frontend-utvidelsen (bane-søk i program-skjemaet: søk anlegg → velg bane →
|
||
importer, samme UI-mønster som spiller-/bane-type-ahead ellers i appen).
|
||
**Rullet ut live 2026-07-18**, bruker bekreftet eksplisitt: migrasjon 010
|
||
kjørt mot ekte `teecup_db` (kun én ny partiell unik-indeks, ingen
|
||
eksisterende rader rørt), begge containere bygget+redeployet, live
|
||
sjekker OK, `teeoff.no` upåvirket. **"Bygg i rekkefølgen ting brukes"-
|
||
planen gjenopptas nå** — blind draw-skjermen er neste steg.
|
||
**Reell produksjonsbug funnet OG fikset samme dag, rapportert av bruker
|
||
som faktisk brukte funksjonen:** brukeren klikket seg korrekt via
|
||
dashbord → turnering → Program-fane (bekreftet med skjermbilde + full
|
||
klikk-sti, ikke gjettet), åpnet "Hent bane fra teeoff", søkte "Tjøme", og
|
||
fikk feilmeldingen "Mangler organisasjon i lenken" — URL-en hadde da
|
||
MISTET `?org=...&name=...`. Root-cause: `OfficialCourseSearch` sitt eget
|
||
søke-`<form onSubmit={runSearch}>` var rendret INNI `CreateSessionCard`
|
||
sitt eksisterende `<form onSubmit={handleSubmit}>` -- nestede
|
||
`<form>`-elementer er ugyldig HTML. Nettleseren slår sammen de to
|
||
skjemaene i den faktiske DOM-en, så "Søk"-knappen submittet i praksis det
|
||
YTRE økt-skjemaet som en ekte native side-navigasjon (GET til gjeldende
|
||
sti, ingen navngitte felt => tom spørrestreng) -- dette vasket bort
|
||
org-parameteren og landet brukeren på siden sin egen org-guard. **Fikset**
|
||
ved å fjerne det indre `<form>`-elementet helt (vanlig `<div>` +
|
||
Enter-tast-håndtering på inputet + `type="button"` i stedet for
|
||
`type="submit"` på søkeknappen) -- gjør nestede skjemaer strukturelt
|
||
umulig fremover for denne komponenten. Ekte typesjekket
|
||
produksjonsbuild kjørt på nytt, kun `teecup_frontend` redeployet (ingen
|
||
backend-endring). **Lærdom for fremtidige skjermer:** en ny
|
||
søk-/underskjema-widget som skal plasseres INNI et eksisterende skjema
|
||
(slik som denne bane-søk-widgeten ligger inni økt-opprett-skjemaet) må
|
||
ALDRI være et eget `<form>` -- bruk `<div>` + eksplisitt
|
||
klikk-/Enter-håndtering.
|
||
|
||
Neste steg:
|
||
1. Blind draw-skjermen (neste i "bygg i rekkefølgen ting brukes", gjenopptatt
|
||
etter ADR-019-sidespranget) — deretter scorekort, leaderboard. Samme
|
||
mønster:
|
||
design i V0 (fortsett i samme prosjekt), FORVENT en full re-eksport hver
|
||
gang — diff mot live-treet i et scratch-område før noe pakkes ut over
|
||
eksisterende filer, og sjekk om V0-skjermen bygger inn handlinger backend
|
||
ikke støtter ennå FØR integrering.
|
||
3. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
|
||
4. Kommunikasjon (chat/feed) — ikke startet.
|