teecup/CLAUDE.md
Erol Haagenrud 6d84aafef8 ADR-018 er nå helt ferdig — backend, begge frontend-skjermer, og nå Open Graph-metadata, alt live samme dag. Oppsummert:
Én reell driftsfeil funnet før den nådde produksjon: generateMetadata() kjører server-side ved forespørselstid — går derfor ikke gjennom next.config.mjs sin rewrites() (som bare gjelder nettleser-trafikk). Den trengte TEECUP_API_ORIGIN direkte, men den variabelen fantes kun i Dockerfile sitt build-steg — ENV fra ett steg arves ikke til det neste i en multi-stage build. Rettet ved å sette den på nytt i runner-steget.

Verifisert presist, ikke bare at det kompilerte: bygget det ekte produksjonsimaget (ikke dev-server), pekte det mot en scratch-backend med ekte data, og hentet den faktiske server-rendrede HTML-en — bekreftet ekte og:title/og:description, og en trygg fallback-tittel for en ukjent turnering.

teeoff.no upåvirket gjennom hele runden.
2026-07-18 10:58:36 +02:00

41 KiB

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_scores 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_selfs 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 filendringenteeoff_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 apiteeoff_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 SELECTtournament 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.pyGET /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.sqllink_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.

Neste steg:

  1. MinIO/bildeopplasting (hero-bilder, sponsorlogoer, og:image) — siste gjenstående punkt fra ADR-018, egen runde.
  2. Flere V0-skjermer (økt/program, blind draw, 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 (se dashboard-/roster-rundene over for hvorfor begge er nødvendige hver gang).
  3. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
  4. Kommunikasjon (chat/feed) — ikke startet.