2026-07-16 08:21:57 +02:00
|
|
|
# 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)
|
ADR-017 er skrevet inn, og migrasjonen er ferdig og verifisert:
007_registration_and_player_fields.sql — kjørt rent gjennom hele kjeden 001→007 på en fersk scratch-database, test_isolation.sql fortsatt 12/12.
Ett reelt arkitekturproblem løst underveis, ikke bare skjema: et offentlig påmeldingskall kjenner en turnering-id, men ingen org-kontekst — og uten den 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 koblingen turnering→org, ingenting annet. Testet presist: kalt som teecup_app-rollen med ingen org-kontekst satt — funksjonen fant riktig org, ga NULL (ikke feil) for en ukjent turnering, og et rått SELECT på samme tilkobling/rolle ga fortsatt 0 rader — beviser at RLS ikke er brutt generelt, bare dette ene smale unntaket finnes.
Ikke gjort ennå (bevisst, dette var kun migrasjonssteget):
Migrasjonen er ikke kjørt mot ekte teecup_db.
Ingen API-endepunkter (offentlig registrerings-router, utvidet players.py).
2026-07-18 08:24:55 +02:00
|
|
|
- `ARCHITECTURE_DECISIONS.md` — hva som er bestemt og hvorfor (ADR-001…017). Fasit.
|
2026-07-16 08:21:57 +02:00
|
|
|
- `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).
|
2026-07-16 09:16:22 +02:00
|
|
|
- 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`.
|
2026-07-16 14:38:42 +02:00
|
|
|
- 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».
|
Ekte autentisering er bygget og verifisert. X-Debug-User-Id-stubben er helt fjernet, ingen fallback beholdt.
Ny flyt: magic-link (POST /auth/request-link → POST /auth/verify-link) + JWT-sesjon i HttpOnly/SameSite=Lax/dynamisk-Secure-cookie (30 dager), pluss /auth/logout og /auth/me. Ny migrasjon 004_auth.sql (unik e-post-indeks + magic_link_token-tabell).
Sikkerhetsdesignet fra Plan-agent-gjennomgangen holdt gjennom testing:
Token: secrets.token_urlsafe(32), kun SHA-256-hash lagres
Atomisk forbruk (UPDATE...RETURNING, ikke les-sjekk-skriv) — hindrer replay
Generisk respons uansett om e-posten finnes — hindrer enumerering
app_user opprettes først ved vellykket verifisering, ikke ved forespørsel — hindrer massopprettelse
Gamle uforbrukte lenker ugyldiggjøres når en ny utstedes
PyJWT (byttet fra python-jose pga. bredere sårbarhetsflate) med eksplisitt algorithms=["HS256"]
Ekte eksistens-sjekk mot app_user på hvert kall — en slettet bruker mister tilgang umiddelbart, ikke etter 30 dager
Alle 12 planlagte tester bestått, inkludert cooldown, token-ugyldiggjøring, utløp, tuklet JWT, slettet bruker, og at debug-headeren nå er helt uten effekt.
To ting funnet og fikset/dokumentert underveis:
ON CONFLICT (email) matchet ikke den nye partielle unike indeksen uten eksplisitt WHERE-klausul — fikset.
En reell, dypere RLS-bug (dokumentert i FEATURE_BACKLOG.md, ikke fikset her): organization-tabellens RLS-policy kaster en 500 i stedet for "se ingenting" når app.current_org leses tilbake som tomstreng (ikke NULL) på en gjenbrukt pool-tilkobling. Berører trolig alle 15 RLS-policyer i skjemaet — for stort og sensitivt (ADR-003-grunnmuren) til å hastefikse her, så jeg mitigerte det lokalt i /auth/me og satte det som punkt 1 i neste-steg-listen.
2026-07-16 15:16:53 +02:00
|
|
|
- **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).
|
RLS-tomstreng-buggen er fikset og verifisert grundig.
Fiksen: ny migrasjon 005_rls_null_guard.sql — én delt STABLE SQL-funksjon app_current_org() (NULLIF(current_setting('app.current_org', true), '')::uuid) erstatter det rå uttrykket i alle 15 RLS-policyer (14 org_isolation + org_self) via ALTER POLICY. NULLIF konverterer tomstreng til NULL før cast, så "trygg standard: se ingenting" gjenopprettes uansett GUC-tilstand.
Verifisert to ganger, ulikt:
Tre nye regresjonstester i test_isolation.sql (Test 10–12): tomstreng-lesing gir 0 rader ikke krasj, tomstreng-skriving avvises av RLS ikke krasj, og org-bootstrap-innsetting (tidligere aldri testet) fungerer rett etter tomstreng-tilstand.
Faktisk gjenskaping av original-buggen mot en ekte container (pool-størrelse tvunget til 1 for å garantere tilkoblings-gjenbruk): varmet opp med et vanlig org_connection()-kall, kalte deretter /auth/me på samme tilkobling — gikk fra 500 til 200.
En viktig ting jeg tok feil om i forrige runde, oppdaget ved å faktisk teste i stedet for å anta: fiksen gjør ikke at /auth/me trygt kan joine organization direkte via plain_connection(). Den løser tomstreng-krasjen, men org_self-policyen krever fortsatt en matchende app.current_org for å vise noen rad i det hele tatt — riktig RLS-design, ikke noe fiksen skulle endre. Siden en bruker kan tilhøre flere organisasjoner, finnes det ingen én kontekst å sette. Rettet til riktig løsning: /auth/me slår nå opp hvert org-navn enkeltvis via org_connection() (verifisert at det faktisk returnerer navnet korrekt).
Alt dokumentert i CLAUDE.md/FEATURE_BACKLOG.md.
2026-07-16 15:34:24 +02:00
|
|
|
**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.
|
2026-07-16 20:41:34 +02:00
|
|
|
- **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.
|
2026-07-16 20:59:59 +02:00
|
|
|
- **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.
|
TeeCup er nå containerisert og live på https://teecup.teeoff.no. Dette var den mest hendelsesrike runden denne økten — første gang noe rørte ekte, permanent infrastruktur, og det viste seg berettiget:
To reelle driftshendelser, begge funnet og rettet i sanntid:
Caddy plukket ikke opp filendringen min — enkeltfil-bind-mount er låst til inoden fra da containeren sist startet; min redigering (atomisk rename) laget en ny inode, så validate/reload/admin-API opererte alle stille på den gamle filen. Løst med full omstart av teeoff_caddy (du bekreftet eksplisitt, siden det avvek fra planens "ingen omstart"-løfte).
Alvorlig nettverkskollisjon — oppdaget rett etterpå da ekte teeoff-trafikk (/api/facilities?... med ekte klubbnavn) dukket opp i teecup_api sin logg. docker-compose.yml sin service-nøkkel api: kolliderte med teeoffs eget api-servicenavn på det delte nettverket — Docker Compose gir service-navn som DNS-alias, så begge containerne delte alias api, og Caddy kunne tilfeldig sende ekte brukertrafikk til TeeCup i stedet. Stoppet teecup_api umiddelbart, ga den navnet teecup_api i stedet, bekreftet kollisjonen er borte.
Mindre ting underveis: jeg eksponerte ved et uhell to secret-verdier i eget debug-output (du roterte passordet), og lærte at Docker ikke leser .env på nytt for en kjørende container (krever --force-recreate etter hver endring) og at # i et upassordet passord kuttes som kommentar av Compose.
Sluttresultat, verifisert ende-til-ende mot den ekte, live stacken: teeoff.no upåvirket gjennom hele prosessen, teecup.teeoff.no/health → 200 med automatisk TLS, full magic-link-innlogging med ekte e-postlevering og Secure-cookie.
CLAUDE.md/FEATURE_BACKLOG.md oppdatert med full detaljer, inkludert en generell lærdom for fremtidige tjenester på delt nettverk. To repoer har uncommittede endringer: /opt/teecup (nye Dockerfile/docker-compose.yml/.dockerignore + statusfiler) og /opt/teeoff (Caddyfile-endringen, egen repo). Ingenting committet ennå — si ifra når du vil det.
2026-07-16 21:47:34 +02:00
|
|
|
- **Containerisert 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.
|
2026-07-17 21:40:42 +02:00
|
|
|
- **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).
|
2026-07-16 08:21:57 +02:00
|
|
|
|
Alt rent, ingen byggeartefakter liggende igjen, dokumentasjonen oppdatert. Oppsummering:
Live og verifisert ende-til-ende:
/verify-siden bygget og live, e-postmalen sender nå en ekte klikkbar lenke (app/email.py/app/config.py)
Frontend containerisert (frontend/Dockerfile, standalone Next.js) og rullet ut som egen teecup_frontend-tjeneste
Caddy peker nå teecup.teeoff.no på frontend-en, som selv proxyer API-kall server-side — same-origin, ingen CORS, cookie uendret (dokumentert som ny ADR-016)
Fant og fikset en reell fallgruve: Next.js sin rewrites() bakes inn ved build-tid for standalone-output, ikke lest ved kjøretid — løst med en Docker build-time ARG
Ekte e-post sendt, ekte lenke klikket, sesjon opprettet — bekreftet av deg
Én ting du må huske: Caddy-endringen ligger i det separate /opt/teeoff-repoet (deploy/Caddyfile), ikke i teecup-repoet — den er ikke committet ennå. Lett å glemme siden resten av denne økten kun har jobbet i /opt/teecup. Vil du at jeg minner deg, eller committer du den nå selv i teeoff-repoet?
CLAUDE.md, FEATURE_BACKLOG.md og ARCHITECTURE_DECISIONS.md (ny ADR-016) er oppdatert. Klar for commit i teecup-repoet når du vil.
2026-07-17 22:28:43 +02:00
|
|
|
- **Frontend 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`.
|
2026-07-18 06:46:09 +02:00
|
|
|
- **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),
|
2026-07-18 06:53:42 +02:00
|
|
|
`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-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
- **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 er skrevet inn, og migrasjonen er ferdig og verifisert:
007_registration_and_player_fields.sql — kjørt rent gjennom hele kjeden 001→007 på en fersk scratch-database, test_isolation.sql fortsatt 12/12.
Ett reelt arkitekturproblem løst underveis, ikke bare skjema: et offentlig påmeldingskall kjenner en turnering-id, men ingen org-kontekst — og uten den 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 koblingen turnering→org, ingenting annet. Testet presist: kalt som teecup_app-rollen med ingen org-kontekst satt — funksjonen fant riktig org, ga NULL (ikke feil) for en ukjent turnering, og et rått SELECT på samme tilkobling/rolle ga fortsatt 0 rader — beviser at RLS ikke er brutt generelt, bare dette ene smale unntaket finnes.
Ikke gjort ennå (bevisst, dette var kun migrasjonssteget):
Migrasjonen er ikke kjørt mot ekte teecup_db.
Ingen API-endepunkter (offentlig registrerings-router, utvidet players.py).
2026-07-18 08:24:55 +02:00
|
|
|
- **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.
|
Registrerings-API-et er live på https://teecup.teeoff.no. Oppsummert:
Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
2026-07-18 08:50:03 +02:00
|
|
|
**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).
|
2026-07-18 09:03:37 +02:00
|
|
|
**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`).
|
Alt rent, ingen byggeartefakter liggende igjen, dokumentasjonen oppdatert. Oppsummering:
Live og verifisert ende-til-ende:
/verify-siden bygget og live, e-postmalen sender nå en ekte klikkbar lenke (app/email.py/app/config.py)
Frontend containerisert (frontend/Dockerfile, standalone Next.js) og rullet ut som egen teecup_frontend-tjeneste
Caddy peker nå teecup.teeoff.no på frontend-en, som selv proxyer API-kall server-side — same-origin, ingen CORS, cookie uendret (dokumentert som ny ADR-016)
Fant og fikset en reell fallgruve: Next.js sin rewrites() bakes inn ved build-tid for standalone-output, ikke lest ved kjøretid — løst med en Docker build-time ARG
Ekte e-post sendt, ekte lenke klikket, sesjon opprettet — bekreftet av deg
Én ting du må huske: Caddy-endringen ligger i det separate /opt/teeoff-repoet (deploy/Caddyfile), ikke i teecup-repoet — den er ikke committet ennå. Lett å glemme siden resten av denne økten kun har jobbet i /opt/teecup. Vil du at jeg minner deg, eller committer du den nå selv i teeoff-repoet?
CLAUDE.md, FEATURE_BACKLOG.md og ARCHITECTURE_DECISIONS.md (ny ADR-016) er oppdatert. Klar for commit i teecup-repoet når du vil.
2026-07-17 22:28:43 +02:00
|
|
|
|
2026-07-16 08:21:57 +02:00
|
|
|
Neste steg:
|
2026-07-18 09:03:37 +02:00
|
|
|
1. Landingssider (turnering/org) med påmeldingsskjema i frontend — egen
|
|
|
|
|
ADR-runde (synlighetsvalg offentlig/org/deltakere, slug-URL for org,
|
|
|
|
|
MinIO for bilder — se `FEATURE_BACKLOG.md`).
|
ADR-017 er skrevet inn, og migrasjonen er ferdig og verifisert:
007_registration_and_player_fields.sql — kjørt rent gjennom hele kjeden 001→007 på en fersk scratch-database, test_isolation.sql fortsatt 12/12.
Ett reelt arkitekturproblem løst underveis, ikke bare skjema: et offentlig påmeldingskall kjenner en turnering-id, men ingen org-kontekst — og uten den 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 koblingen turnering→org, ingenting annet. Testet presist: kalt som teecup_app-rollen med ingen org-kontekst satt — funksjonen fant riktig org, ga NULL (ikke feil) for en ukjent turnering, og et rått SELECT på samme tilkobling/rolle ga fortsatt 0 rader — beviser at RLS ikke er brutt generelt, bare dette ene smale unntaket finnes.
Ikke gjort ennå (bevisst, dette var kun migrasjonssteget):
Migrasjonen er ikke kjørt mot ekte teecup_db.
Ingen API-endepunkter (offentlig registrerings-router, utvidet players.py).
2026-07-18 08:24:55 +02:00
|
|
|
2. Flere V0-skjermer (økt/program, blind draw, scorekort, leaderboard) —
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
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).
|
ADR-017 er skrevet inn, og migrasjonen er ferdig og verifisert:
007_registration_and_player_fields.sql — kjørt rent gjennom hele kjeden 001→007 på en fersk scratch-database, test_isolation.sql fortsatt 12/12.
Ett reelt arkitekturproblem løst underveis, ikke bare skjema: et offentlig påmeldingskall kjenner en turnering-id, men ingen org-kontekst — og uten den 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 koblingen turnering→org, ingenting annet. Testet presist: kalt som teecup_app-rollen med ingen org-kontekst satt — funksjonen fant riktig org, ga NULL (ikke feil) for en ukjent turnering, og et rått SELECT på samme tilkobling/rolle ga fortsatt 0 rader — beviser at RLS ikke er brutt generelt, bare dette ene smale unntaket finnes.
Ikke gjort ennå (bevisst, dette var kun migrasjonssteget):
Migrasjonen er ikke kjørt mot ekte teecup_db.
Ingen API-endepunkter (offentlig registrerings-router, utvidet players.py).
2026-07-18 08:24:55 +02:00
|
|
|
3. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
|
|
|
|
|
4. Kommunikasjon (chat/feed) — ikke startet.
|