# 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…014). 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. Neste steg: 1. Containerisere TeeCup-API-et (Dockerfile + compose-tjeneste), koble mot `teecup_db` med `teecup_app`, rute via eksisterende Caddy til `teecup.teeoff.no`. (Under scratch-verifisering måtte hele `/opt/teecup` monteres, ikke bare `app/`, fordi `handicap_engine.py` er et toppnivå-søskenmodul til `app`-pakken — Dockerfilen må `COPY` begge inn med samme relative plassering.) Ekte SMTP-utsending av magic-link må også kobles inn før dette går live (i dag: dev-only logging bak `TEECUP_DEV_LOG_MAGIC_LINKS`). 2. Deretter frontend (PWA, offline-first) og kommunikasjon (migrasjon 006, siden 004/005 nå er tatt av auth og RLS-fiksen).