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-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
- `ARCHITECTURE_DECISIONS.md` — hva som er bestemt og hvorfor (ADR-001…018). 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`).
|
ADR-018 sin backend er ferdig, verifisert og live. Oppsummert:
Én reell feil funnet og rettet underveis, ikke antatt riktig: migrasjonen feilet først mot scratch — organization.slug har faktisk ligget i skjemaet siden migrasjon 001 ("f.eks. subdomene/URL-vennlig"), noe jeg hadde oversett og prøvde å legge til på nytt. Rettet, kjørte rent etterpå.
En viktig presisering oppdaget under bygging, ikke antatt på forhånd: RLS beskytter kun tenant-grenser (org A ser aldri org B), ikke innholds-synlighet innenfor riktig org-kontekst. Det gamle offentlige endepunktet fra forrige runde leste faktisk fullt innhold uten noen synlighetssjekk i det hele tatt — synlighet må håndheves eksplisitt i koden, noe jeg nå har gjort konsekvent på både lesing og registrering.
Fylte et implisitt hull: ADR-en beskrev synligheten, men ingen tidligere runde hadde bygget en vei for organisator til å faktisk sette disse feltene — lagt til PATCH-endepunkter for turnering og org, pluss full sponsor-CRUD.
Grundig testet: hele synlighetsmatrisen med ekte HTTP-kall — inkludert den interessante "kylling-og-egg"-konsekvensen av Beslutning D (ingen kan selv-registrere seg til en participants-synlig turnering, kun organisator kan legge til direkte — riktig, ikke en bug).
Live nå, teeoff.no upåvirket gjennom hele prosessen.
2026-07-18 09:45:11 +02:00
|
|
|
|
- **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.
|
2026-07-18 10:32:53 +02:00
|
|
|
|
- **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.
|
Klubb-landingssiden er live på https://teecup.teeoff.no/clubs/{slug} — dermed er ADR-018 sine to planlagte frontend-skjermer komplette.
Én reell komponentkonflikt løst, ikke duplisert bort: TournamentCard var bygget kun for den innloggede dashbord-konteksten. I stedet for å lage en egen kopi for den offentlige klubbsiden, gjorde jeg orgId valgfri — satt gir innlogget lenke, utelatt gir /t/{id} i stedet. Samme kort, to kontekster. Bekreftet dashbordet fortsatt fungerer uendret.
V0 laget selv en rute denne gangen, men kalte parameteren [id] selv om det faktisk er en slug — skrev en egen, riktig navngitt [slug]-rute i stedet.
Verifisert mot ekte scratch-data gjennom en kjørende frontend-dev-server: en org med to turneringer (én offentlig, én org-privat) — klubbsiden viste kun den offentlige, akkurat som filteret i API-et tilsier.
Live, teeoff.no upåvirket. Gjenstår av ADR-018: Open Graph-metadata for delingsforhåndsvisning, og MinIO/bilder som egen runde. Vil du ta Open Graph-metadataen nå, siden det er en liten, avgrenset bit?
2026-07-18 10:48:20 +02:00
|
|
|
|
- **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.
|
2026-07-18 10:58:36 +02:00
|
|
|
|
- **Open Graph-metadata LIVE (2026-07-18), samme dag — ADR-018 dermed
|
|
|
|
|
|
helt ferdig:** `generateMetadata()` lagt til på `/t/[id]` og
|
|
|
|
|
|
`/clubs/[slug]` (ekte tittel/beskrivelse fra API-et, `og:site_name`,
|
|
|
|
|
|
trygg fallback-tittel ved ukjent id/slug — feil her skal ALDRI hindre
|
|
|
|
|
|
selve siden i å laste).
|
|
|
|
|
|
**Reell driftsfeil funnet FØR den nådde produksjon, ikke etter:**
|
|
|
|
|
|
`generateMetadata()` kjører server-side ved REQUEST-tid, ikke i
|
|
|
|
|
|
nettleseren — går derfor IKKE gjennom `next.config.mjs` sin
|
|
|
|
|
|
`rewrites()` (som kun gjelder nettleser-trafikk inn til Next.js-
|
|
|
|
|
|
serveren). Måtte derfor lese `TEECUP_API_ORIGIN` direkte, men den
|
|
|
|
|
|
variabelen fantes KUN i `Dockerfile` sitt builder-steg — `ENV` satt i
|
|
|
|
|
|
ett `FROM`-steg arves ikke til et senere. Rettet ved å sette samme
|
|
|
|
|
|
`ARG`/`ENV` på nytt i runner-steget også.
|
|
|
|
|
|
**Verifisert presist at fiksen faktisk virker, ikke bare at bygget gikk
|
|
|
|
|
|
gjennom:** bygget det EKTE produksjonsimaget (ikke dev-server) pekt mot
|
|
|
|
|
|
en scratch-backend med en ekte offentlig turnering+org, hentet den
|
|
|
|
|
|
faktiske server-rendrede HTML-en og bekreftet ekte `<title>`/`og:title`/
|
|
|
|
|
|
`og:description` — ikke bare at TypeScript kompilerte. Ukjent
|
|
|
|
|
|
turnering-id ga korrekt trygg fallback-tittel.
|
|
|
|
|
|
**Bevisst utenfor omfang:** `og:image` — ingen ekte bilde finnes ennå
|
|
|
|
|
|
(MinIO-runden). La til `metadataBase` i `app/layout.tsx` nå likevel, som
|
|
|
|
|
|
forarbeid slik at et fremtidig relativt bilde-URL løses riktig uten en
|
|
|
|
|
|
egen fiks da.
|
|
|
|
|
|
**Rullet ut live**, kun `teecup_frontend`, `teeoff.no` upåvirket.
|
MinIO-runden er ferdig, verifisert og live — ADR-018 er nå helt komplett, ingenting utsatt igjen bortsett fra selve opplastings-skjermen i frontend.
Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd.
To reelle feil funnet under scratch-verifisering, aldri i produksjon:
pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker.
MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur.
Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet.
Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden.
Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført.
Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
2026-07-18 11:25:52 +02:00
|
|
|
|
- **MinIO-runden LIVE (2026-07-18), samme dag — ADR-018 dermed HELT
|
|
|
|
|
|
ferdig, ingenting utsatt igjen:** ny `teecup-minio`-tjeneste (persistent
|
|
|
|
|
|
volum, genererte credentials i `.env`), backend-endepunkter for ekte
|
|
|
|
|
|
bildeopplasting til `tournament.hero_image_key`/`tournament_sponsor.
|
|
|
|
|
|
logo_key` (som lå inerte siden migrasjon 009), offentlige API-svar bygger
|
|
|
|
|
|
nå fulle URL-er, `og:image` koblet på i `/t/[id]` sin `generateMetadata`.
|
|
|
|
|
|
**Sent, men viktig presisert krav underveis:** brukeren avbrøt en
|
|
|
|
|
|
verktøyskall midtveis for å presisere at ALLE bilder skal konverteres
|
|
|
|
|
|
til AVIF for plassbesparelse. Snudde HELE opplastingsarkitekturen på
|
|
|
|
|
|
dette: droppet den opprinnelige planen om presignerte URL-er (nettleser
|
|
|
|
|
|
laster opp DIREKTE til MinIO) til fordel for ekte multipart-opplasting
|
|
|
|
|
|
GJENNOM API-et, som konverterer til AVIF (Pillow + pillow-avif-plugin)
|
|
|
|
|
|
FØR lagring. Dette forenklet arkitekturen betydelig: kun ÉN MinIO-klient
|
|
|
|
|
|
trengs nå (før: to separate klient-oppsett for henholdsvis internt
|
|
|
|
|
|
admin-arbeid og offentlig presignering), og Caddy sin nye rute trenger
|
|
|
|
|
|
ikke lenger bevare Host-headeren presist (relevant kun for SigV4-
|
|
|
|
|
|
signaturverifisering av presignerte URL-er, ikke for anonym public-read).
|
|
|
|
|
|
**To reelle feil funnet UNDER scratch-verifisering, aldri i produksjon:**
|
|
|
|
|
|
(1) `pillow-avif-plugin` krever ingen ekstra systempakker i
|
|
|
|
|
|
`python:3.12-slim` -- verifisert med et frittstående encode/decode-
|
|
|
|
|
|
rundtrip FØR det ble tatt i bruk i selve API-et. (2) MinIO validerer
|
|
|
|
|
|
`Host`-headeren STRENGT og avviser understrek som ugyldig vertsnavn --
|
|
|
|
|
|
et tjenestenavn med understrek (`teecup_minio`, konsistent med
|
|
|
|
|
|
`teecup_api`/`teecup_frontend`) feilet umiddelbart ved oppstart
|
|
|
|
|
|
("Invalid Request (invalid hostname)"). Bekreftet presist ved å teste
|
|
|
|
|
|
samme oppsett med bindestrek i stedet (`teecup-minio`) -- fungerte
|
|
|
|
|
|
umiddelbart. Alle referanser rettet til bindestrek FØR noe ble forsøkt
|
|
|
|
|
|
mot ekte infrastruktur.
|
|
|
|
|
|
**Caddy:** ny `/teecup-media/*`-rute på det EKSISTERENDE
|
|
|
|
|
|
`teecup.teeoff.no`-blokket (IKKE et nytt subdomene -- en tidlig sjekk
|
|
|
|
|
|
avdekket at `media.teecup.teeoff.no` fantes som en wildcard DNS-post,
|
|
|
|
|
|
men pekte til en IPv6-adresse denne serveren ikke har i det hele tatt;
|
|
|
|
|
|
path-prefiks på et allerede fungerende domene unngikk hele den DNS-
|
|
|
|
|
|
avhengigheten). Samme stale-inode-oppførsel som alle tidligere Caddy-
|
|
|
|
|
|
runder -- full `docker restart teeoff_caddy`, brukeren bekreftet
|
|
|
|
|
|
eksplisitt. `teeoff.no` upåvirket.
|
|
|
|
|
|
**Verifisert grundig, i flere lag:** frittstående AVIF-encode/decode-
|
|
|
|
|
|
test, full scratch-kjede (fersk Postgres + en ISOLERT scratch-MinIO-
|
|
|
|
|
|
container) med et ekte opplastet bilde -- bekreftet konvertert til
|
|
|
|
|
|
gyldig AVIF (800×400, 406 bytes for et helfarget testbilde), bekreftet
|
|
|
|
|
|
lagret med riktig nøkkel, bekreftet lesbart ANONYMT direkte mot MinIO
|
|
|
|
|
|
(uten Caddy, isolerer bucket-policyen), bekreftet `hero_image_url`/
|
|
|
|
|
|
`logo_url` bygget riktig i det offentlige API-svaret. Alle tre
|
|
|
|
|
|
valideringsveier testet (ugyldig content-type, korrupt bildeinnhold,
|
|
|
|
|
|
for stor fil >8MB) -- alle ga korrekt `VALIDATION_FAILED`. Etter
|
|
|
|
|
|
Caddy-omstart: et ekte anonymt kall mot `/teecup-media/...` på
|
|
|
|
|
|
produksjonsdomenet ga en ekte MinIO S3-XML-feilrespons (`NoSuchKey` for
|
|
|
|
|
|
en scratch-nøkkel som naturligvis ikke finnes i prod) -- beviser ruten
|
|
|
|
|
|
treffer MinIO selv, ikke frontend sin egen 404-side.
|
|
|
|
|
|
`test_isolation.sql` fortsatt 12/12 gjennom hele runden.
|
|
|
|
|
|
**Bevisst utenfor omfang:** ingen faktisk opplasting av et EKTE bilde
|
|
|
|
|
|
til en EKTE, live turnering i denne runden (ville krevd å skrive
|
|
|
|
|
|
test-data i brukerens ekte konto uten å bli spurt) -- tilbudt, ikke
|
|
|
|
|
|
utført. Selve dra-og-slipp-opplastingsskjermen i frontend (V0) er
|
|
|
|
|
|
fortsatt ikke bygget, som avtalt fra starten av runden.
|
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
|
|
|
|
|
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
|
|
|
|
- **Program-skjerm (økter/tidsplan), bygget og SCRATCH-verifisert, IKKE
|
|
|
|
|
|
live ennå (2026-07-18):** sjette V0-skjerm, første i "bygg i rekkefølgen
|
|
|
|
|
|
ting brukes"-serien (etter lag/roster: program → blind draw → scorekort →
|
|
|
|
|
|
leaderboard). `components/tournament-program.tsx`, ny rute
|
|
|
|
|
|
`/tournaments/[id]/program`. Tidslinje over økter + opprett-skjema
|
|
|
|
|
|
(format, hullomfang, scoringsmodus, poeng, klokkeslett+intervall,
|
|
|
|
|
|
starthull, kollapsbar "avansert"-seksjon med ADR-014s fire brytere).
|
|
|
|
|
|
**Reelt blokkerende hull funnet FØR integrering:** `SessionCreate.
|
|
|
|
|
|
course_id` er påkrevd, men INGEN endepunkt kunne noensinne produsere en —
|
|
|
|
|
|
ADR-004s teeoff-integrasjon er fortsatt bare vedtatt (ingen utgående
|
|
|
|
|
|
HTTP-klient finnes). Spurte bruker eksplisitt — svar: bygg enkel
|
|
|
|
|
|
course-CRUD nå. Ny `app/routers/courses.py` (`GET`/`POST /orgs/{id}/
|
|
|
|
|
|
courses`, kun `source='custom'`). Program-skjemaet fikk et
|
|
|
|
|
|
type-ahead-felt for bane (samme mønster som spiller-type-ahead i
|
|
|
|
|
|
roster-skjermen).
|
|
|
|
|
|
**Reell korrekthetsfeil rettet FØR integrering:** V0-promptet mitt ba om
|
|
|
|
|
|
ett generisk "Scramble"-valg, men skjemaets `CHECK`-constraint og
|
|
|
|
|
|
`handicap_engine.py` sin `Format`-enum krever `scramble_2`/`scramble_4`
|
|
|
|
|
|
som distinkte verdier -- ren `"scramble"` avvises med 400. Rettet i
|
|
|
|
|
|
frontend-mappingen til to segment-knapper.
|
|
|
|
|
|
**`allowance_override`-JSON-formen verifisert eksakt** mot
|
|
|
|
|
|
`app/handicap.py` sin `parse_allowance_config`/`_strategy_from_json`
|
|
|
|
|
|
(`{type: "combined"|"per_player", percentage: 0..1}`, ikke en flat
|
|
|
|
|
|
prosent) -- frontend velger riktig `type` ut fra om formatet er
|
|
|
|
|
|
side-enhet eller spiller-enhet, konverterer 0–100-skjemafelt til 0–1.
|
|
|
|
|
|
**Scratch-infrastruktur denne runden, bevisst forskjellig fra tidligere:**
|
|
|
|
|
|
brukte en ISOLERT `teecup_app_scratch`-rolle (`GRANT teecup_app TO
|
|
|
|
|
|
teecup_app_scratch`) i stedet for den ekte `teecup_app`-rollen -- den er
|
|
|
|
|
|
nå cluster-global og produksjonskritisk (delt Postgres-instans med
|
|
|
|
|
|
`teecup_db`), så et eldre plandokuments "drop teecup_app-rolle"-
|
|
|
|
|
|
opprydning (skrevet FØR go-live) er utdatert og ble bevisst IKKE fulgt.
|
|
|
|
|
|
Egen isolert scratch-MinIO-container også (app-oppstart krever en
|
|
|
|
|
|
nåbar MinIO for `ensure_bucket()`).
|
|
|
|
|
|
**Verifisert:** courses opprettet+listet, kryss-org-isolasjon bekreftet,
|
|
|
|
|
|
økt med klokkeslett, økt med `scramble_4`+full `allowance_override`-
|
|
|
|
|
|
rundtur, gammel `"scramble"`-verdi avvist (400), `test_isolation.sql`
|
|
|
|
|
|
12/12, ekte typesjekket PRODUKSJONSBUILD (samme `Dockerfile` som faktisk
|
|
|
|
|
|
deployes) kjørt og bekreftet.
|
|
|
|
|
|
**Diffet V0-eksporten mot live-treet FØR noe ble tatt inn** (samme mønster
|
|
|
|
|
|
som alle tidligere runder): kun tre reelt nye filer, resten forventede
|
|
|
|
|
|
full-reverts, ikke rørt. Fjernet V0s dev-only forhåndsvisnings-toggle;
|
|
|
|
|
|
lagt til fanerad ("Lag og spillere" / "Program") i BEGGE skjermene siden
|
|
|
|
|
|
V0 ikke visste om den andre når den ble generert i egen prompt.
|
2026-07-18 12:29:24 +02:00
|
|
|
|
**Rullet ut live 2026-07-18**, bruker bekreftet eksplisitt: `docker
|
|
|
|
|
|
compose up -d --build teecup_api teecup_frontend` (kun disse to, `teecup-
|
|
|
|
|
|
minio` urørt). Verifisert: begge containere boot-et rent (`Application
|
|
|
|
|
|
startup complete`, Next.js `Ready`), `teecup.teeoff.no/health` og
|
|
|
|
|
|
`/dashboard` → 200, `teeoff.no` upåvirket (200).
|
|
|
|
|
|
- **ADR-004 (teeoff-banedata) kartlagt, IKKE bygget (2026-07-18):** brukeren
|
|
|
|
|
|
påpekte rett etter program-skjerm-rullingen at ADR-004s teeoff-
|
|
|
|
|
|
integrasjon fortsatt bare er vedtatt, ikke bygget (kun `source='custom'`
|
|
|
|
|
|
finnes). Kartlagt `/opt/teeoff/backend/main.py` (annet repo, kun lest):
|
|
|
|
|
|
`GET /api/facilities/{slug}` (offentlig, INGEN auth/API-nøkkel) returnerer
|
|
|
|
|
|
allerede alt teecup trenger — `courses[].holes[]` (`par`, `hcp_index`
|
|
|
|
|
|
= stroke index), `courses[].tees[]` (`name`, `cr_men`/`slope_men`,
|
|
|
|
|
|
`cr_women`/`slope_women` — kjønnsdelt WHS-rating). CORS-lista på teeoff-
|
|
|
|
|
|
siden ekskluderer teecups origin, men er IRRELEVANT for et server-til-
|
|
|
|
|
|
server-kall (kun nettleser-fetch rammes av CORS). Ingen dokumentasjon
|
|
|
|
|
|
(README/OpenAPI) finnes for dette API-et i teeoff-repoet — kartleggingen
|
|
|
|
|
|
over er basert på å lese koden direkte. Stabil identifikator: `facilities.
|
|
|
|
|
|
slug` (f.eks. `borregaard-golfklubb`) — selve banen har kun en intern
|
|
|
|
|
|
serial-id, ingen egen slug, så `course.external_course_ref` må bære
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
facility-slug + bane-id sammen.
|
|
|
|
|
|
**Oppdatering samme dag: brukeren ba meg ta fatt på dette NÅ** (bevisst
|
|
|
|
|
|
sidesprang fra "bygg i rekkefølgen ting brukes"-planen — blind draw-
|
|
|
|
|
|
skjermen er fortsatt neste steg i den planen ETTERPÅ, ikke droppet).
|
|
|
|
|
|
Design besluttet og skrevet som **ADR-019** (se ARCHITECTURE_DECISIONS.md):
|
|
|
|
|
|
import (kopi) ved organisators eksplisitte valg, ikke live oppslag ved
|
|
|
|
|
|
hver bruk — fryser data på importtidspunktet, samme reproduserbarhets-
|
|
|
|
|
|
prinsipp som handicap-snapshotten (ADR-007). Server-til-server-kall
|
|
|
|
|
|
(`teecup_api` → `http://teeoff_api:8000`, internt Docker-nettverk, ingen
|
|
|
|
|
|
auth trengs). Ny migrasjon `010` (unik `external_course_ref` per org,
|
2026-07-18 15:54:40 +02:00
|
|
|
|
hindrer dupliserte importer). Se ADR-019 for alle fem delbeslutningene.
|
|
|
|
|
|
**Bygget, scratch-verifisert MOT EKTE `teeoff_api`** (ikke en simulert
|
|
|
|
|
|
respons — ekte HTTP-kall til den kjørende produksjonscontaineren, kun
|
|
|
|
|
|
lesing): søkte opp "Borregaard" i teeoff sine 174 publiserte anlegg,
|
|
|
|
|
|
hentet Borregaard Golfklubb sin hovedbane (18 hull, 4 tee-farger ×
|
|
|
|
|
|
kjønn), importerte den til en scratch-org — alle 18 hull med riktig
|
|
|
|
|
|
par/stroke-index (1-18, unik), alle 8 tee/tee_rating-rader med riktig
|
|
|
|
|
|
full_18 course/slope-rating verifisert direkte i databasen, opprettet
|
|
|
|
|
|
deretter en ekte økt med den importerte banen som `course_id` (beviser
|
|
|
|
|
|
hele veien til handicap-motoren fungerer, ikke bare selve importen).
|
|
|
|
|
|
Reimport av samme bane korrekt avvist (409 DUPLICATE, migrasjon 010 sin
|
|
|
|
|
|
indeks). Kryss-org-isolasjon bekreftet, ukjent teeoff-slug ga korrekt 404,
|
|
|
|
|
|
`test_isolation.sql` 12/12. Ekte typesjekket produksjonsbuild av
|
|
|
|
|
|
frontend-utvidelsen (bane-søk i program-skjemaet: søk anlegg → velg bane →
|
|
|
|
|
|
importer, samme UI-mønster som spiller-/bane-type-ahead ellers i appen).
|
|
|
|
|
|
**Rullet ut live 2026-07-18**, bruker bekreftet eksplisitt: migrasjon 010
|
|
|
|
|
|
kjørt mot ekte `teecup_db` (kun én ny partiell unik-indeks, ingen
|
|
|
|
|
|
eksisterende rader rørt), begge containere bygget+redeployet, live
|
|
|
|
|
|
sjekker OK, `teeoff.no` upåvirket. **"Bygg i rekkefølgen ting brukes"-
|
|
|
|
|
|
planen gjenopptas nå** — blind draw-skjermen er neste steg.
|
|
|
|
|
|
**Reell produksjonsbug funnet OG fikset samme dag, rapportert av bruker
|
|
|
|
|
|
som faktisk brukte funksjonen:** brukeren klikket seg korrekt via
|
|
|
|
|
|
dashbord → turnering → Program-fane (bekreftet med skjermbilde + full
|
|
|
|
|
|
klikk-sti, ikke gjettet), åpnet "Hent bane fra teeoff", søkte "Tjøme", og
|
|
|
|
|
|
fikk feilmeldingen "Mangler organisasjon i lenken" — URL-en hadde da
|
|
|
|
|
|
MISTET `?org=...&name=...`. Root-cause: `OfficialCourseSearch` sitt eget
|
|
|
|
|
|
søke-`<form onSubmit={runSearch}>` var rendret INNI `CreateSessionCard`
|
|
|
|
|
|
sitt eksisterende `<form onSubmit={handleSubmit}>` -- nestede
|
|
|
|
|
|
`<form>`-elementer er ugyldig HTML. Nettleseren slår sammen de to
|
|
|
|
|
|
skjemaene i den faktiske DOM-en, så "Søk"-knappen submittet i praksis det
|
|
|
|
|
|
YTRE økt-skjemaet som en ekte native side-navigasjon (GET til gjeldende
|
|
|
|
|
|
sti, ingen navngitte felt => tom spørrestreng) -- dette vasket bort
|
|
|
|
|
|
org-parameteren og landet brukeren på siden sin egen org-guard. **Fikset**
|
|
|
|
|
|
ved å fjerne det indre `<form>`-elementet helt (vanlig `<div>` +
|
|
|
|
|
|
Enter-tast-håndtering på inputet + `type="button"` i stedet for
|
|
|
|
|
|
`type="submit"` på søkeknappen) -- gjør nestede skjemaer strukturelt
|
|
|
|
|
|
umulig fremover for denne komponenten. Ekte typesjekket
|
|
|
|
|
|
produksjonsbuild kjørt på nytt, kun `teecup_frontend` redeployet (ingen
|
|
|
|
|
|
backend-endring). **Lærdom for fremtidige skjermer:** en ny
|
|
|
|
|
|
søk-/underskjema-widget som skal plasseres INNI et eksisterende skjema
|
|
|
|
|
|
(slik som denne bane-søk-widgeten ligger inni økt-opprett-skjemaet) må
|
|
|
|
|
|
ALDRI være et eget `<form>` -- bruk `<div>` + eksplisitt
|
|
|
|
|
|
klikk-/Enter-håndtering.
|
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
|
|
|
|
|
2026-07-18 16:19:52 +02:00
|
|
|
|
- **Organisator-overstyring i `team_authz.py`, LIVE (2026-07-18):** reist av
|
|
|
|
|
|
brukeren rett før blind draw-skjermen skulle designes: `app/team_authz.py`
|
|
|
|
|
|
sin `user_may_act_for_team` krevde tidligere en `team_roster`-rad med
|
|
|
|
|
|
lenket bruker-konto — ingen vei for organisatoren til å låse/legge til
|
|
|
|
|
|
deltakere/føre score hvis ingen spiller hadde logget inn ennå (vanlig
|
|
|
|
|
|
tidlig i en turnering). Utvidet til også å godta org-eier/admin
|
|
|
|
|
|
(`organization_membership.role IN ('owner','admin')`), på ETHVERT lag,
|
|
|
|
|
|
ingen unntak for at organisatoren selv er rostret på motstanderlaget.
|
|
|
|
|
|
**Det unntaket ble bevisst vurdert og avvist** (brukeren spurte selv om
|
|
|
|
|
|
det, jeg anbefalte det opprinnelig, men vi kom sammen frem til at det ville
|
|
|
|
|
|
skapt en verre låsning: er organisatoren spillende på Lag A og
|
|
|
|
|
|
Lag B heller ikke har noen innlogget spiller, ville Lag B blitt helt
|
|
|
|
|
|
låst ute) — TeeCup er et tillitsbasert klubb-/vennegjeng-verktøy, ikke en
|
|
|
|
|
|
sikkerhetsgrense mot en fiendtlig organisator som uansett allerede ser
|
|
|
|
|
|
begge lags fulle troppe-liste (blind draw skjuler kun selve
|
|
|
|
|
|
kamp-paringen). Alle 5 kallsteder oppdatert (matches.py sin
|
|
|
|
|
|
add_participant/lock_lineup, scoring.py sin submit_hole_score/
|
|
|
|
|
|
submit_hole_result). **Verifisert grundig i scratch:** org-eier uten
|
|
|
|
|
|
roster kan nå låse BEGGE lag + føre score, en vanlig 'member'-rolle
|
|
|
|
|
|
fortsatt blokkert (uendret), en rostret spiller fungerer uendret
|
|
|
|
|
|
uavhengig av org-rolle. `test_isolation.sql` 12/12. Ingen migrasjon (ren
|
|
|
|
|
|
Python-endring) — kun `teecup_api` redeployet, `teeoff.no` upåvirket.
|
|
|
|
|
|
|
2026-07-18 17:15:50 +02:00
|
|
|
|
- **Tee-endepunkter bygget og LIVE (2026-07-18), rett før blind draw-skjermen:**
|
|
|
|
|
|
fant et hull som blokkerte selve blind draw-flyten: `match_participant.
|
|
|
|
|
|
tee_id` er påkrevd, men det fantes INGEN `GET`-vei for å liste en banes
|
|
|
|
|
|
tee-er (selv offisielt importerte baner har tee-rader, men ingenting
|
|
|
|
|
|
eksponerte dem) OG egendefinerte («custom») baner har ALDRI hatt noen
|
|
|
|
|
|
vei til å FÅ tee-er i det hele tatt — dette er ikke noe ADR-019 innførte,
|
|
|
|
|
|
det var et hull som fantes fra før custom-baner ble lagt til i første
|
|
|
|
|
|
omgang, bare usynlig til nå. Konsekvens før fiksen: en turnering satt opp
|
|
|
|
|
|
på en manuelt navngitt bane kunne ALDRI få en ekte deltaker lagt til på
|
|
|
|
|
|
noen match. **Presisering (brukeren spurte eksplisitt):** dette er IKKE
|
|
|
|
|
|
noe som må løses i teeoff.no sin kode — egendefinerte baner er per
|
|
|
|
|
|
definisjon baner teeoff ikke kjenner til, så dette er en ren
|
|
|
|
|
|
teecup-intern funksjon, uavhengig av ADR-019-integrasjonen.
|
|
|
|
|
|
Ny `GET/POST /orgs/{id}/courses/{id}/tees` i `app/routers/courses.py`.
|
|
|
|
|
|
`POST` avviser eksplisitt forsøk på offisielle baner (400
|
|
|
|
|
|
`VALIDATION_FAILED` — de får tee-ene sine fra teeoff-importen, ikke
|
|
|
|
|
|
manuelt). Kun full_18-rating dekket (samme begrunnelse som ADR-019
|
|
|
|
|
|
Beslutning D). **Bevisst UTENFOR omfang denne runden, egen senere sak:**
|
|
|
|
|
|
hull-/stroke-index-data for egendefinerte baner (`hole`-tabellen forblir
|
|
|
|
|
|
tom for custom-baner) — trengs for korrekt slagfordeling i SCORING-fasen
|
|
|
|
|
|
(ADR-008), ikke i blind draw, så det løses naturlig når scorekort-
|
|
|
|
|
|
skjermen bygges (samme "bygg i rekkefølgen ting brukes"-logikk).
|
|
|
|
|
|
**Verifisert grundig i scratch:** tom tee-liste på fersk custom-bane,
|
|
|
|
|
|
opprett+list tee på custom-bane, offisiell bane viser alle 8 importerte
|
|
|
|
|
|
tee-er, manuell tee-opprettelse avvist på offisiell bane, og — den
|
|
|
|
|
|
faktiske payoff-en — en deltaker lagt til en match på en custom-bane-økt
|
|
|
|
|
|
for FØRSTE gang noensinne (`POST .../matches/{id}/participants` lykkes
|
|
|
|
|
|
nå med en tee_id fra en nyopprettet custom-tee). `test_isolation.sql`
|
|
|
|
|
|
12/12. Ingen migrasjon, kun `teecup_api` redeployet, `teeoff.no`
|
|
|
|
|
|
upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
- **Blind draw-skjermen LIVE (2026-07-18):** syvende V0-skjerm,
|
|
|
|
|
|
`components/session-blind-draw.tsx`, ny rute
|
|
|
|
|
|
`/tournaments/[id]/sessions/[sessionId]`. To lag-kolonner, hver med sine
|
|
|
|
|
|
matcher ("flights"), legg til/fjern spiller+tee per plass (antall plasser
|
|
|
|
|
|
avhenger av format), lås-knapp med bekreftelse, avslørings-visning når
|
|
|
|
|
|
begge lag har låst. Program-skjermens økt-kort er nå klikkbare inn hit.
|
|
|
|
|
|
**Datamodell tilpasset fra V0s mock til API-ets faktiske sett-modell:**
|
|
|
|
|
|
V0 designet faste, indekserte "Slot"-arrays (kontrollert skjema-state);
|
|
|
|
|
|
API-et har verken PATCH på `match_participant` eller noen slot-indeks —
|
|
|
|
|
|
kun opprett/slett av en løs deltaker-mengde per side. Løst med en
|
|
|
|
|
|
"legg til spiller"-inline-form (samme mønster som spiller-/bane-type-ahead
|
|
|
|
|
|
ellers i appen) i stedet for faste dropdown-rader; funksjonelt likeverdig,
|
|
|
|
|
|
strukturelt riktigere for API-ets faktiske form.
|
|
|
|
|
|
**To reelle hull funnet og fikset FØR/UNDER integrering:**
|
|
|
|
|
|
1. Ingen `DELETE` fantes for `match_participant` — en kaptein kunne aldri
|
|
|
|
|
|
angre et valg før låsing uten å etterlate en foreldreløs rad. Ny
|
|
|
|
|
|
`DELETE /orgs/{id}/matches/{match_id}/participants/{participant_id}`
|
|
|
|
|
|
(samme ALREADY_LOCKED/NOT_ROSTERED_ON_TEAM-sjekker som opprett).
|
|
|
|
|
|
2. **"Skriv blindt"-hull, funnet under selve scratch-testingen (ikke bare
|
|
|
|
|
|
tenkt ut på forhånd):** forrige rundes org-admin-overstyring i
|
|
|
|
|
|
`team_authz.py` lot en organisator LEGGE TIL deltakere på et lag de
|
|
|
|
|
|
ikke selv er rostret på, men `app/blind_draw.py` sin `own_team_ids()`
|
|
|
|
|
|
sjekket KUN rostret spiller for SYNLIGHET — så organisatoren så aldri
|
|
|
|
|
|
sine egne tilføyelser igjen før begge lag hadde låst. Fikset ved å gi
|
|
|
|
|
|
`own_team_ids()` samme owner/admin-utvidelse som `user_may_act_for_
|
|
|
|
|
|
team()` — en org-admin ser nå BEGGE lag umiddelbart (konsistent med at
|
|
|
|
|
|
de uansett allerede har full tilgang, se forrige rundes resonnement),
|
|
|
|
|
|
mens en faktisk rostret kaptein fortsatt kun ser sitt eget lag før
|
|
|
|
|
|
reveal. Kun ett reelt kallsted (`matches.py` sin `list_matches`).
|
|
|
|
|
|
**Tredje, urelatert bug fanget under samme scratch-økt og fikset på
|
|
|
|
|
|
brukerens eksplisitte forespørsel:** `POST .../matches/{id}/participants`
|
|
|
|
|
|
krasjet med en rå 500 (`TypeError` i `handicap_engine.py` sin
|
|
|
|
|
|
`course_handicap_raw`) hvis spilleren manglet `handicap_index` OG
|
|
|
|
|
|
`use_handicap` var på (standard) — preeksisterende, ikke noe denne
|
|
|
|
|
|
runden introduserte. Fikset i `app/routers/matches.py` sin
|
|
|
|
|
|
`add_participant`: sjekker nå `handicap_index_snapshot IS NULL` EKSPLISITT
|
|
|
|
|
|
FØR innsetting når `use_handicap` er sann, avviser med en klar
|
|
|
|
|
|
`VALIDATION_FAILED` (400) i stedet for å krasje. Bekreftet at
|
|
|
|
|
|
`use_handicap=false` fortsatt tillater en spiller uten handicap
|
|
|
|
|
|
(scratch-spill), og at en spiller MED handicap fortsatt fungerer uendret.
|
|
|
|
|
|
**Verifisert grundig i scratch, flere runder:** DELETE-endepunktet
|
|
|
|
|
|
(fjern+idempotent 404 ved gjentak+409 etter lås), synlighetsfikset (org-
|
|
|
|
|
|
admin ser begge sider, rostret kaptein ser fortsatt kun eget lag),
|
|
|
|
|
|
null-handicap-fikset (avvist rent, scratch-modus upåvirket, normal
|
|
|
|
|
|
spiller upåvirket), full opprett-match→legg-til-deltaker→lås→avslør-
|
|
|
|
|
|
syklus ende-til-ende. `test_isolation.sql` 12/12 etter hver runde. Ekte
|
|
|
|
|
|
typesjekket produksjonsbuild.
|
|
|
|
|
|
**Rullet ut live**, bruker bekreftet eksplisitt: begge containere
|
|
|
|
|
|
bygget+redeployet (ingen migrasjon), `teeoff.no` upåvirket.
|
|
|
|
|
|
|
2026-07-18 17:47:33 +02:00
|
|
|
|
- **Backend-forarbeid for scorekort-skjermen, LIVE (2026-07-18):** to hull
|
|
|
|
|
|
funnet ved gjennomlesing av `scoring.py` FØR V0-prompten ble skrevet,
|
|
|
|
|
|
samme føre-var-mønster som resten av økten.
|
|
|
|
|
|
1. **Hull-/stroke-index-data for egendefinerte baner** (bevisst utsatt fra
|
|
|
|
|
|
tee-rundens status-notat) — `hole`-tabellen har ALDRI hatt noen vei inn
|
|
|
|
|
|
for custom-baner, som blokkerte `stroke`-scoringsmodus helt (`hole_
|
|
|
|
|
|
result`-modus trenger den ikke). Ny `GET/POST /orgs/{id}/courses/{id}/
|
|
|
|
|
|
holes` i `app/routers/courses.py` — engangs alle-18-på-en-gang
|
|
|
|
|
|
(avviser feil antall, avviser hvis banen allerede har hull, avviser på
|
|
|
|
|
|
offisielle baner som får hullene sine fra teeoff-import).
|
|
|
|
|
|
2. **`GET scorecard` viste kun UTLEDET vinn/tap/delt per hull, aldri de
|
|
|
|
|
|
faktiske tallene som var registrert** — umulig å bygge en skjerm som
|
|
|
|
|
|
viser gjeldende tilstand ved gjenlasting. Utvidet `Scorecard`-modellen
|
|
|
|
|
|
i `app/routers/scoring.py` med `stroke_entries`/`hole_result_entries`
|
|
|
|
|
|
(rå `hole_score`/`match_hole_result`-rader, nøyaktig ett av de to fylt
|
|
|
|
|
|
ut avhengig av øktens scoring_mode).
|
|
|
|
|
|
**Verifisert grundig i scratch:** hull-validering (feil antall avvist,
|
|
|
|
|
|
gjentatt oppsett avvist), og en FULL `stroke`-modus scoringsrunde med
|
|
|
|
|
|
ekte handicap-justert nettoberegning på en egendefinert bane for FØRSTE
|
|
|
|
|
|
gang noensinne (ulikt slagtall — 4 mot 5 — ga korrekt "halved" etter
|
|
|
|
|
|
handicap-utjevning), scorecard sin nye `stroke_entries` bekreftet å
|
|
|
|
|
|
returnere nøyaktig det som ble registrert. `test_isolation.sql` 12/12.
|
|
|
|
|
|
**Rullet ut live**, ingen migrasjon, kun `teecup_api` redeployet,
|
|
|
|
|
|
`teeoff.no` upåvirket.
|
|
|
|
|
|
|
|
|
|
|
|
- **Scorekort-skjermen LIVE (2026-07-18):** åttende V0-skjerm,
|
|
|
|
|
|
`components/session-scorecard.tsx`, ny rute `/tournaments/[id]/sessions/
|
|
|
|
|
|
[sessionId]/matches/[matchId]`. Ett hull i fokus om gangen (banebruk,
|
|
|
|
|
|
ikke et regneark) — store slag-steppere for `stroke`-modus, tre-valgs
|
|
|
|
|
|
vinner-knapper for `hole_result`-modus, hull-chip-navigasjon, kollapsbar
|
|
|
|
|
|
full oversikt, feiret "avgjort"-banner som låser alt til read-only.
|
|
|
|
|
|
Blind draw sin avslørte visning lenker nå til hver match sitt scorekort.
|
|
|
|
|
|
**Ingen nye backend-hull denne runden** — forrige rundes forarbeid
|
|
|
|
|
|
(hull-endepunkter + `stroke_entries`/`hole_result_entries` i scorecard)
|
|
|
|
|
|
dekket akkurat det skjermen trengte.
|
|
|
|
|
|
**Verifisert grundig i scratch, inkludert et fullt oppsett som speiler
|
|
|
|
|
|
NØYAKTIG frontend-ens egen last-sekvens** (sessions→teams→matches→
|
|
|
|
|
|
holes→scorecard, deretter en ekte `POST hole-scores` for begge spillere
|
|
|
|
|
|
på hull 1 og en refetch): status_text/derivert resultat oppdaterte seg
|
|
|
|
|
|
korrekt ("1 UP (A)", hull 1 → "a"), alle feltnavn stemte eksakt med
|
|
|
|
|
|
TypeScript-typene uten justering. `test_isolation.sql` 12/12, ekte
|
|
|
|
|
|
typesjekket build.
|
|
|
|
|
|
**Rullet ut live**, ren frontend-endring, `teeoff.no` upåvirket.
|
|
|
|
|
|
|
2026-07-18 18:22:10 +02:00
|
|
|
|
- **Leaderboard-backend LIVE (2026-07-18):** ny `GET /orgs/{id}/
|
|
|
|
|
|
tournaments/{id}/leaderboard` i `app/routers/tournaments.py` — summerer
|
|
|
|
|
|
poeng på tvers av ALLE økter/matcher i turneringen, både total og
|
|
|
|
|
|
per-økt-delsum.
|
|
|
|
|
|
**Reelt korrekthetshull funnet OG designet rundt FØR koden ble skrevet,
|
|
|
|
|
|
ikke oppdaget i ettertid:** `match.team_a_id`/`team_b_id` settes PER
|
|
|
|
|
|
MATCH ved opprettelse, ikke garantert konsistent på tvers av matcher —
|
|
|
|
|
|
en organisator kunne i prinsippet opprettet match 1 med team_a=Rød og
|
|
|
|
|
|
match 2 med team_a=Blå. En naiv summering av `points_side_a`/`points_
|
|
|
|
|
|
side_b` ville da blandet sammen poeng fra to ULIKE fysiske lag. Løst ved
|
|
|
|
|
|
å ALDRI summere på "a"/"b"-labelen — kun på det ekte lag-id-et
|
|
|
|
|
|
(`points_by_team: dict[team_id, float]`, både i totalen og per økt).
|
|
|
|
|
|
**Verifisert presist, ikke bare "kjørte uten feil":** bygget et scratch-
|
|
|
|
|
|
scenario med 3 matcher over 2 økter der match 2 sin team_a/team_b
|
|
|
|
|
|
BEVISST var byttet om i forhold til de to andre — satte poeng direkte
|
|
|
|
|
|
(Rød vinner alle tre, ett delt) og bekreftet at leaderboardet likevel ga
|
|
|
|
|
|
riktig total (Rød 2.5, Blå 0.5) og riktig per-økt-delsum, til tross for
|
|
|
|
|
|
swap-en. `test_isolation.sql` 12/12. Ingen migrasjon, kun `teecup_api`
|
|
|
|
|
|
redeployet, `teeoff.no` upåvirket. Frontend-skjermen gjenstår.
|
|
|
|
|
|
|
|
|
|
|
|
- **Leaderboard-skjermen LIVE (2026-07-18), samme dag:** niende og siste
|
|
|
|
|
|
V0-skjerm i "bygg i rekkefølgen ting brukes"-serien for kamp-play-flyten.
|
|
|
|
|
|
`components/tournament-leaderboard.tsx`, ny rute `/tournaments/[id]/
|
|
|
|
|
|
leaderboard`. Stort scoreboard-kort med de to lagenes totalpoeng
|
|
|
|
|
|
(lederen fremhevet), pluss en per-økt poeng-fordelingsliste med
|
|
|
|
|
|
fullført/ikke-startet-status. V0 la selv til et tredje faneelement
|
|
|
|
|
|
("Leaderboard") i BÅDE program- og roster-skjermens fanerad denne
|
|
|
|
|
|
runden — portert inn i de LIVE versjonene av begge (med riktig `org`-
|
|
|
|
|
|
parameter, som V0s eksport som vanlig manglet).
|
|
|
|
|
|
**Ingen nye backend-hull** — forrige rundes leaderboard-endepunkt dekket
|
|
|
|
|
|
akkurat det skjermen trengte.
|
|
|
|
|
|
**Verifisert i scratch:** full datamodell-runde (samme felt-for-felt
|
|
|
|
|
|
som backend-verifiseringen dagen før) OG et eget tomtilstand-scenario
|
|
|
|
|
|
(fersk turnering med 2 lag, 0 økter — `sessions: []`, `matches_total: 0`)
|
|
|
|
|
|
for å bekrefte at "ingen økter opprettet ennå"-meldingen vises riktig
|
|
|
|
|
|
i stedet for å krasje på et tomt array. `test_isolation.sql` 12/12, ekte
|
|
|
|
|
|
typesjekket build.
|
|
|
|
|
|
**Rullet ut live**, ren frontend-endring, `teeoff.no` upåvirket.
|
|
|
|
|
|
**Hele "bygg i rekkefølgen ting brukes"-serien for match-play-flyten er
|
|
|
|
|
|
dermed komplett:** oppsett (lag/roster) → program → blind draw →
|
|
|
|
|
|
scorekort → leaderboard.
|
2026-07-18 22:21:22 +02:00
|
|
|
|
- **Offisiell bane-import: idempotent + tydeligere navn, LIVE (2026-07-18),
|
|
|
|
|
|
rapportert av brukeren som faktisk brukte funksjonen på ekte
|
|
|
|
|
|
produksjonsdata:** to reelle problemer, begge funnet ved å lese (kun
|
|
|
|
|
|
lesing) de faktiske dataene for "De Gamle er Eldst" FØR noe ble antatt.
|
|
|
|
|
|
1. **`POST .../courses/official-import` var IKKE idempotent** — å
|
|
|
|
|
|
importere samme bane på nytt (helt vanlig: flere økter spilles ofte
|
|
|
|
|
|
på samme bane) ga en 409 `DUPLICATE`-feil (migrasjon 010 sin sperre)
|
|
|
|
|
|
i stedet for å bare gi tilbake den allerede importerte banen. Fikset:
|
|
|
|
|
|
sjekker nå `external_course_ref` FØR noe teeoff-kall gjøres — finnes
|
|
|
|
|
|
banen fra før, returneres den eksisterende raden direkte (også
|
|
|
|
|
|
raskere, og robust mot at teeoff er nede akkurat da).
|
|
|
|
|
|
2. **Lagret navn var kun selve banens navn ("Hovedbanen"), ikke hvilken
|
|
|
|
|
|
klubb** — ubrukelig til å skille baner fra hverandre, siden mange
|
|
|
|
|
|
klubber navngir hovedbanen sin identisk. Fikset: navnet kombineres nå
|
|
|
|
|
|
til "{anlegg} – {bane}" (f.eks. "Tjøme Golfklubb – Hovedbanen") ved
|
|
|
|
|
|
import.
|
|
|
|
|
|
**Reell konsekvens av hull #1 funnet i produksjonsdata:** brukeren hadde,
|
|
|
|
|
|
mens hen forsøkte å søke opp Tjøme via det ØVERSTE banefeltet (som søker
|
|
|
|
|
|
organisasjonens EGNE baner, ikke teeoff), ved et uhell trigget «Opprett
|
|
|
|
|
|
ny bane: «Tj»»-snarveien og fått en tom, søppel `custom`-bane hengende på
|
|
|
|
|
|
Foursome-økten -- en ekte forvekslingsfelle mellom de to adskilte
|
|
|
|
|
|
bane-søkeflatene (eget vs. teeoff), ikke en kodefeil i seg selv.
|
|
|
|
|
|
**Data ryddet opp i EKTE `teecup_db`, bruker bekreftet eksplisitt:**
|
|
|
|
|
|
Foursome-økten pekt om til den allerede importerte, ekte Tjøme-banen
|
|
|
|
|
|
(samme bane som Fourball-økten allerede brukte -- nøyaktig det
|
|
|
|
|
|
organisatoren egentlig ønsket), søppel-«Tj»-banen slettet (bekreftet
|
|
|
|
|
|
ingen matcher/tee-er/hull hang på den FØR sletting), og den ekte banens
|
|
|
|
|
|
navn oppdatert til "Tjøme Golfklubb – Hovedbanen". Verifisert i etterkant
|
|
|
|
|
|
at begge økter nå peker til samme, korrekt navngitte bane.
|
|
|
|
|
|
**Verifisert mot ekte teeoff_api i scratch FØR utrulling:** importerte
|
|
|
|
|
|
Borregaard på nytt to ganger — andre kallet ga nøyaktig samme course-id
|
|
|
|
|
|
(ikke en duplikat-rad), navnet kom ut som "Borregaard Golfklubb –
|
|
|
|
|
|
Hovedbanen". `test_isolation.sql` 12/12. Ingen migrasjon, kun `teecup_api`
|
|
|
|
|
|
redeployet, `teeoff.no` upåvirket.
|
|
|
|
|
|
- **`PATCH`/`DELETE` for økter, LIVE (2026-07-18), samme dag:** brukeren
|
|
|
|
|
|
spurte rett etter opprydningen om det i det hele tatt var mulig å
|
|
|
|
|
|
rette/slette en feiloppsatt økt — det var det ikke (kun `POST`/`GET`
|
|
|
|
|
|
fantes). Ny `PATCH /orgs/{id}/sessions/{id}` (`app/routers/
|
|
|
|
|
|
tournaments.py`) for enkle felt (navn, klokkeslett/intervall, starthull,
|
|
|
|
|
|
poeng, handicap-brytere) via vanlig `exclude_unset`-mønster, PLUSS en egen
|
|
|
|
|
|
gren for bane-bytte. Ny `DELETE /orgs/{id}/sessions/{id}` — kun tomme
|
|
|
|
|
|
økter (ingen matcher), avviser med 409 ellers (bruk PATCH til å korrigere
|
|
|
|
|
|
i stedet).
|
|
|
|
|
|
**Banebytte-scenarioet brukeren selv reiste** ("5 hull spilt, oppdager
|
|
|
|
|
|
feil bane — slagene er ekte, utregningen er trolig feil") krevde egen
|
|
|
|
|
|
design: `match_participant.tee_id` peker til en tee som HØRER til den
|
|
|
|
|
|
gamle banen. Løst med `_remap_course()` — finner en tee med samme
|
|
|
|
|
|
navn+kjønn på den nye banen for hver allerede tillagte deltaker, flytter
|
|
|
|
|
|
dem dit, og avviser HELE bane-byttet tydelig (400, ingenting skrevet,
|
|
|
|
|
|
bekreftet transaksjonell rollback) hvis den nye banen mangler en
|
|
|
|
|
|
tilsvarende tee. Etter et vellykket bytte: handicap regnes om for alle
|
|
|
|
|
|
berørte deltakere, og matchstatus/poeng regnes om for HVER match i
|
|
|
|
|
|
økten — **bevisst uavhengig av om matchen allerede er avgjort** (brukeren
|
|
|
|
|
|
bekreftet eksplisitt at en bane-korrigering skal kunne endre et allerede
|
|
|
|
|
|
cachet resultat). `recompute_and_cache_match_state` i `app/routers/
|
|
|
|
|
|
scoring.py` gjort delt (fjernet ledende understrek) for gjenbruk fra
|
|
|
|
|
|
tournaments.py.
|
|
|
|
|
|
**Reelt, urelatert funn underveis i scratch-testingen, IKKE fikset:**
|
|
|
|
|
|
`tee_rating` lages i dag ALLTID kun med `full_18`-omfang (både ved
|
|
|
|
|
|
teeoff-import og manuell tee-opprettelse) — en økt satt til `front_9`/
|
|
|
|
|
|
`back_9` i `stroke`-modus kan derfor ALDRI få handicap beregnet
|
|
|
|
|
|
(`compute_and_store_side_handicaps` sin `tee_rating`-join finner aldri
|
|
|
|
|
|
noen rad), og dermed aldri avgjøre noen hull. `hole_result`-modus
|
|
|
|
|
|
upåvirket. Flagget til bruker, bevisst latt urørt denne runden.
|
|
|
|
|
|
**Verifisert grundig i scratch:** enkelt feltbytte (kun navn), fullt
|
|
|
|
|
|
banebytte-scenario bygget nøyaktig som brukerens eksempel (10 hull spilt
|
|
|
|
|
|
under feil bane, matchen allerede avgjort 10&8, PATCH til riktig bane →
|
|
|
|
|
|
tee-er ombyttet korrekt, handicap endret fra 7/9 til 10/13 under den nye
|
|
|
|
|
|
banens rating, matchstatus regnet på nytt med UENDREDE rå slagtall),
|
|
|
|
|
|
avvist bane-bytte ved manglende tee-match (bekreftet full rollback,
|
|
|
|
|
|
også av det urelaterte navnefeltet i samme kall), DELETE avvist på økt
|
|
|
|
|
|
med match (409) og godtatt på tom økt (204). `test_isolation.sql` 12/12.
|
|
|
|
|
|
Ingen migrasjon, kun `teecup_api` redeployet, `teeoff.no` upåvirket.
|
|
|
|
|
|
Frontend (rediger-/slett-knapper i program-skjermen) ikke bygget ennå —
|
|
|
|
|
|
kun backend-kapasiteten denne runden.
|
2026-07-19 08:17:12 +02:00
|
|
|
|
- **Rediger/slett-UI for økter LIVE (2026-07-19):** V0-utvidelse av den
|
|
|
|
|
|
eksisterende Program-skjermen (ingen ny rute) — "..."-meny (samme
|
|
|
|
|
|
DropdownMenu-mønster som roster-skjermens per-spiller-handlinger) med
|
|
|
|
|
|
"Rediger"/"Slett" per øktkort. Rediger bytter kortet til et inline-skjema
|
|
|
|
|
|
(samme felt som PATCH støtter: navn, poeng, starthull, klokkeslett/
|
|
|
|
|
|
intervall, bane). Slett viser en bekreftelse; treffer den ekte 409-en
|
|
|
|
|
|
(økt har matcher), vises backend sin egen feiltekst i stedet for en
|
|
|
|
|
|
forhåndsberegnet klient-tilstand.
|
|
|
|
|
|
**Reell regresjon funnet OG UNNGÅTT i selve V0-eksporten, ikke i
|
|
|
|
|
|
etterkant:** denne rundens V0-prompt handlet kun om rediger/slett, men
|
|
|
|
|
|
eksporten hadde samtidig (utilsiktet) FJERNET bane-feltet fra "Legg til
|
|
|
|
|
|
økt"-skjemaet helt — nye økter ville stille blitt satt til en hardkodet
|
|
|
|
|
|
mock-bane. Fanget under diff-mot-live-treet FØR noe ble tatt inn (samme
|
|
|
|
|
|
rutine som alltid) — `CreateSessionCard` (med sitt ekte bane-søk/
|
|
|
|
|
|
teeoff-import) beholdt fullstendig urørt; kun de nye rediger/slett-
|
|
|
|
|
|
delene ble hentet inn.
|
|
|
|
|
|
**Bane-bytte i redigeringsskjemaet er bevisst ENKLERE enn opprett-
|
|
|
|
|
|
skjemaets bane-felt** — kun velg blant organisasjonens eksisterende
|
|
|
|
|
|
baner eller hent fra teeoff, INGEN "opprett ny bane: X"-snarvei her
|
|
|
|
|
|
(PATCH sin `course_id` må være en ekte, allerede eksisterende bane, ikke
|
|
|
|
|
|
en tekststreng) — unngår at samme "Tj"-forvekslingsfelle fra forrige
|
|
|
|
|
|
banebytte-hendelse kan gjenta seg via redigeringsveien.
|
|
|
|
|
|
**Verifisert:** ekte PATCH med nøyaktig skjemaets feltform, ekte DELETE
|
|
|
|
|
|
(204 tom økt, 409 med matcher — bekreftet at `{"detail":{"code",
|
|
|
|
|
|
"message"}}`-formen leses riktig av UI-et), `test_isolation.sql` 12/12,
|
|
|
|
|
|
ekte typesjekket build. Rullet ut live, ren frontend-endring, `teeoff.no`
|
|
|
|
|
|
upåvirket.
|
2026-07-18 18:22:10 +02:00
|
|
|
|
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
|
- **Invitasjonskode + ledende side + projisert stilling, BACKEND LIVE
|
|
|
|
|
|
(2026-07-19, ADR-020):** brukeren reiste tre relaterte hull rett etter at
|
|
|
|
|
|
"bygg i rekkefølgen ting brukes"-serien var ferdig: (1) ingen vei inn til
|
|
|
|
|
|
en turnering for en spiller som bare har fått muntlig beskjed, (2)
|
|
|
|
|
|
leaderboardet viser kun faktisk opptjente poeng, ikke hva stillingen ville
|
|
|
|
|
|
blitt om pågående matcher holder seg, (3) ingen fargekoding i matchlister
|
|
|
|
|
|
for hvem som leder. Full ADR-020 skrevet (4 delbeslutninger, se
|
|
|
|
|
|
ARCHITECTURE_DECISIONS.md) — nøkkelbeslutning bekreftet eksplisitt av
|
|
|
|
|
|
bruker FØR bygging: en invitasjonskode OVERSTYRER `tournament.visibility`
|
|
|
|
|
|
helt (koden ER selve invitasjonen, ikke en snarvei som fortsatt krever
|
|
|
|
|
|
eksisterende tilgang).
|
|
|
|
|
|
Ny migrasjon `011_join_code_and_leading_side.sql`: `tournament.join_code`
|
|
|
|
|
|
(6 tegn, alfabet uten 0/O/1/I, globalt unikt, backfylt for eksisterende
|
|
|
|
|
|
rader), `match.leading_side` (cachet fortegn av `MatchState.lead`, samme
|
|
|
|
|
|
mønster som `status_text`/`points_side_a/b`), fjerde
|
|
|
|
|
|
`SECURITY DEFINER`-bro `public_tournament_by_code()` (etter
|
|
|
|
|
|
`public_tournament_org` 007, `link_player_by_email` 008,
|
|
|
|
|
|
`public_org_by_slug` 009).
|
|
|
|
|
|
**Backend bygget:** `create_tournament` genererer koden (retry-løkke ved
|
|
|
|
|
|
kollisjon, astronomisk usannsynlig med 33^6 kombinasjoner). Ny
|
|
|
|
|
|
`GET /public/tournaments/by-code/{code}` (MÅ registreres FØR
|
|
|
|
|
|
`/{tournament_id}` i routeren, ellers tolkes "by-code" som en ugyldig
|
|
|
|
|
|
UUID). `GET /public/tournaments/{id}`, `GET .../sessions` og
|
|
|
|
|
|
`POST .../register` godtar alle en valgfri `code`-parameter som — når den
|
|
|
|
|
|
matcher — hopper `_check_visibility()` helt over.
|
|
|
|
|
|
`recompute_and_cache_match_state` (scoring.py) cacher nå `leading_side`
|
|
|
|
|
|
ved HVER hull-innsending, ikke bare ved avgjørelse. Leaderboard-
|
|
|
|
|
|
endepunktet fikk `projected_points`/`projected_points_by_team`:
|
|
|
|
|
|
avgjorte matcher bidrar likt til faktisk og projisert, ikke-avgjorte gir
|
|
|
|
|
|
hele `points_per_match` til `leading_side` (delt 0,5/0,5 ved "AS"/ikke
|
|
|
|
|
|
startet) — speiler hvordan Ryder Cup-TV-dekning viser "hvis det sluttet
|
|
|
|
|
|
nå".
|
|
|
|
|
|
**Reell hendelse underveis, håndtert transparent (ikke skjult):** en
|
|
|
|
|
|
feilformulert `docker exec teeoff_db env | grep -i POSTGRES`-kommando
|
|
|
|
|
|
(ment å liste variabelNAVN) fanget opp `POSTGRES_PASSWORD` sin VERDI også,
|
|
|
|
|
|
siden selve nøkkelnavnet matchet søkemønsteret — eksponerte `teeoff_db`
|
|
|
|
|
|
sitt superbruker-passord (`teeoff_admin`) i verktøyresultatet. Alvorligere
|
|
|
|
|
|
enn de to tidligere passord-hendelsene i prosjektet siden dette er
|
|
|
|
|
|
superbrukeren for HELE den delte Postgres-klyngen (teeoff OG teecup), ikke
|
|
|
|
|
|
en enkelt tjeneste-credential. Flagget til bruker umiddelbart, som valgte
|
|
|
|
|
|
å rotere. Rotert trygt UTEN å noensinne re-eksponere gammel ELLER ny verdi
|
|
|
|
|
|
i noe synlig kommandoresultat: `ALTER ROLE` kjørt via lokal
|
|
|
|
|
|
Unix-socket-`trust`-auth (bekreftet ved å lese `pg_hba.conf`, ingen
|
|
|
|
|
|
hemmelighet involvert i den sjekken) — krevde altså IKKE det gamle
|
|
|
|
|
|
passordet i det hele tatt. Nytt passord generert med `openssl rand -hex
|
|
|
|
|
|
32` (hex, ikke base64 — unngår SAMME klasse URL-enkodings-felle som
|
|
|
|
|
|
`TEECUP_DATABASE_URL`-hendelsen tidligere, siden verdien også ligger i en
|
|
|
|
|
|
`postgres://`-DSN). `/opt/teeoff/.env` sine TO forekomster
|
|
|
|
|
|
(`POSTGRES_PASSWORD` og `DATABASE_URL`) oppdatert med `sed`-mønstre som
|
|
|
|
|
|
ALDRI leser/skriver ut den gamle verdien. `teeoff_api`/`teeoff_worker`
|
|
|
|
|
|
service-nøklene i `docker-compose.prod.yml` viste seg å hete `api`/
|
|
|
|
|
|
`worker` (ikke `teeoff_api`/`teeoff_worker` — det er kun
|
|
|
|
|
|
`container_name`), samme "service-nøkkel ≠ container-navn"-fallgruve som
|
|
|
|
|
|
nettverksalias-hendelsen fra containeriseringsrunden. `docker compose up
|
|
|
|
|
|
-d --force-recreate api worker` gjenskapte OGSÅ `teeoff_db` selv (ikke
|
|
|
|
|
|
eksplisitt navngitt) — Compose oppdager konfigurasjonsendring
|
|
|
|
|
|
(`${POSTGRES_PASSWORD}` i `db`-tjenestens egen `environment:`) og
|
|
|
|
|
|
gjenskaper uansett hvilke tjenester som ble navngitt. Verifisert grundig
|
|
|
|
|
|
ETTERPÅ: `teeoff_db`-loggen viste "Skipping initialization" (datavolum
|
|
|
|
|
|
urørt, ikke reinitialisert) + ren oppstart, `teeoff_api`/`teeoff_worker`
|
|
|
|
|
|
ren oppstart uten en eneste feil-/auth-/passord-linje i hele loggen,
|
|
|
|
|
|
`teeoff.no` OG `teecup.teeoff.no/health` begge `200` etterpå.
|
|
|
|
|
|
**Scratch-verifisert grundig** (fersk `teecup_scratch` 001→011,
|
|
|
|
|
|
isolert `teecup_app_scratch`-rolle, isolert scratch-MinIO, engangs
|
|
|
|
|
|
API-container): `test_isolation.sql` 12/12 (måtte først rettes — tre
|
|
|
|
|
|
RÅ `INSERT INTO tournament`-steder i selve testfilen predaterte
|
|
|
|
|
|
`join_code` og traff den nye NOT NULL-constrainten, rettet med
|
|
|
|
|
|
dummy-koder). Full ende-til-ende-runde: to turneringer opprettet, ulike
|
|
|
|
|
|
koder bekreftet; `by-code`-oppslag bekreftet for kjent OG ukjent kode
|
|
|
|
|
|
(404); anonym lesing av en `org`-synlig turnering BLOKKERT uten kode
|
|
|
|
|
|
(403 NOT_VISIBLE), TILLATT med riktig kode (inkl. case-insensitivt),
|
|
|
|
|
|
FORTSATT blokkert med feil kode; samme mønster bekreftet for
|
|
|
|
|
|
`POST .../register`. Full hull-for-hull-simulering av en singel-match
|
|
|
|
|
|
(hole_result-modus): `leading_side`/`status_text` fulgte hverandre
|
|
|
|
|
|
eksakt gjennom "1 UP (A)" → "AS" (leading_side=null) → avgjort "9&7
|
|
|
|
|
|
(A)", leaderboardets `projected_points` traff nøyaktig 1.0/0.0 mens A
|
|
|
|
|
|
ledet, 0.5/0.5 ved "AS", og ble likt `points`/`projected_points` (begge
|
|
|
|
|
|
1.0/0.0) etter avgjørelse.
|
|
|
|
|
|
**Rullet ut mot ekte `teecup_db` 2026-07-19**, bruker bekreftet
|
|
|
|
|
|
eksplisitt: migrasjon 011 kjørt (eneste eksisterende turnering fikk
|
|
|
|
|
|
automatisk generert kode), `test_isolation.sql` fortsatt 12/12, kun
|
|
|
|
|
|
`teecup_api` redeployet (ingen frontend-endring i denne del-runden),
|
|
|
|
|
|
`teeoff.no` upåvirket.
|
|
|
|
|
|
**Frontend fullført samme dag, egen del-runde:** `login-form.tsx` fikk et
|
|
|
|
|
|
eget kode-modus (`JoinByCode`) — «Har du en invitasjonskode?»-lenke bytter
|
|
|
|
|
|
ut e-post-skjemaet, slår opp `/public/tournaments/by-code/{code}` og
|
|
|
|
|
|
navigerer til `/t/{id}?code=...` med Next sin `useRouter`. `code`
|
|
|
|
|
|
query-param tres gjennom hele veien: `app/t/[id]/page.tsx` leser
|
|
|
|
|
|
`searchParams`, `public-tournament.tsx` sender den med på BÅDE
|
|
|
|
|
|
info-/sessions-lesingen og selve `POST .../register` (ikke bare det
|
|
|
|
|
|
første oppslaget som tok deg dit).
|
|
|
|
|
|
`tournament-detail.tsx` viser koden i en egen kopier-chip i headeren —
|
|
|
|
|
|
ingen enkelt-turnering-`GET` fantes, så komponenten henter i stedet hele
|
|
|
|
|
|
org-ens turneringsliste (som allerede bærer `join_code`) og finner egen
|
|
|
|
|
|
rad, i stedet for å legge til et nytt endepunkt kun for dette.
|
|
|
|
|
|
`tournament-leaderboard.tsx` fikk en ny `SegmentedBar`-komponent — ETT
|
|
|
|
|
|
fargesegmentert rektangel per bar (ikke tall side om side), proporsjonalt
|
|
|
|
|
|
med hvert lags poeng, 50/50 nøytralt ved 0-0. To slike bares rett under
|
|
|
|
|
|
headeren: "Stilling nå" (faktisk) og "Projisert (hvis pågående matcher
|
|
|
|
|
|
holder seg)" — den EKSISTERENDE store tall-scoreboarden beholdt uendret
|
|
|
|
|
|
lenger ned som detaljvisning.
|
|
|
|
|
|
`session-blind-draw.tsx` sin `RevealedView`: matchkortet får nå en farget
|
|
|
|
|
|
toppkant (leaderens `team.color`) og en `status_text`-chip (fylt farge
|
|
|
|
|
|
ved avgjort match m/ konfetti-ikon, lys tone ved pågående) i stedet for
|
|
|
|
|
|
kun klokkeslettet; `RevealSide` for ledende side får en svak fargetonet
|
|
|
|
|
|
bakgrunn. `leading_side`/`status_text`/`points_side_a/b` lagt til
|
|
|
|
|
|
`ApiMatch`-typen (var der allerede i API-et, bare ikke konsumert
|
|
|
|
|
|
frontend-siden før nå).
|
|
|
|
|
|
**Verifisert:** ekte typesjekket produksjonsbuild (samme `Dockerfile`
|
|
|
|
|
|
som deployes, ikke dev-server) kompilerte rent, alle 10 ruter listet.
|
|
|
|
|
|
Rullet ut (kun `teecup_frontend`, ingen backend-endring i denne delen),
|
|
|
|
|
|
`teecup.teeoff.no/dashboard` og `/` → 200, `teeoff.no` upåvirket. Ekte
|
|
|
|
|
|
smoke-test i produksjon: `by-code`-oppslag for "De Gamle er Eldst" sin
|
|
|
|
|
|
faktiske kode ga riktig turnering-id, `/t/{id}?code=...` ga 200.
|
|
|
|
|
|
**ADR-020 er dermed helt ferdig** (backend + frontend, alle fire
|
|
|
|
|
|
del-ønsker: kode-basert oppdagelse, kode-felt på login, projisert
|
|
|
|
|
|
stilling, fargekoding av matcher) — bortsett fra kode-regenerering, som
|
|
|
|
|
|
er bevisst utsatt (se FEATURE_BACKLOG.md).
|
|
|
|
|
|
**Reell driftshendelse underveis** (mellom backend- og frontend-delen,
|
|
|
|
|
|
under scratch-oppsett): en feilformulert `grep -i POSTGRES`-kommando
|
|
|
|
|
|
eksponerte `teeoff_db` sitt superbruker-passord ved et uhell. Flagget
|
|
|
|
|
|
umiddelbart, brukeren valgte å rotere — se detaljene under
|
|
|
|
|
|
backend-avsnittet over for hele hendelsen og hvordan roteringen ble
|
|
|
|
|
|
gjennomført uten å noensinne re-eksponere gammel eller ny verdi.
|
2026-07-19 10:05:42 +02:00
|
|
|
|
- **Sesjons-bug diagnostisert og FIKSET, LIVE (2026-07-19):** brukeren
|
|
|
|
|
|
rapporterte at hen måtte be om ny magic-link-kode ved HVERT besøk til
|
|
|
|
|
|
`teecup.teeoff.no`, til tross for ADR-009s 30-dagers sesjonscookie. Bad
|
|
|
|
|
|
brukeren sjekke den EKTE cookien i nettleseren fremfor å gjette — kom
|
|
|
|
|
|
tilbake korrekt satt i alle henseender (`Expires` 30 dager frem,
|
|
|
|
|
|
`Secure`/`HttpOnly`/`SameSite=Lax`). Rot-årsaken var derfor IKKE cookien
|
|
|
|
|
|
eller backend-en: `frontend/app/page.tsx` (rot-siden) viste ALLTID
|
|
|
|
|
|
innloggingsskjemaet uten noensinne å sjekke om en gyldig sesjon allerede
|
|
|
|
|
|
fantes. `Dashboard`-komponenten sjekker `/auth/me` og sender til `/` ved
|
|
|
|
|
|
MANGLENDE sesjon, men ingen kode gjorde det motsatte — en bruker som
|
|
|
|
|
|
besøkte roten direkte (i stedet for å navigere til `/dashboard`) så
|
|
|
|
|
|
derfor alltid innloggingsskjemaet uansett sesjonsstatus.
|
|
|
|
|
|
**Fikset:** `page.tsx` gjort om til en async server-komponent som leser
|
|
|
|
|
|
sesjonscookien via `next/headers`, kaller `/auth/me` server-til-server
|
|
|
|
|
|
direkte mot `TEECUP_API_ORIGIN` (samme mønster som `generateMetadata` i
|
|
|
|
|
|
`app/t/[id]/page.tsx` — IKKE gjennom `next.config.mjs` sin `rewrites()`,
|
|
|
|
|
|
som kun gjelder nettleser-trafikk), og sender en allerede innlogget
|
|
|
|
|
|
bruker videre til `/dashboard` med `redirect()` FØR innloggingsskjemaet
|
|
|
|
|
|
når rendres.
|
|
|
|
|
|
**Verifisert presist mot den ekte, live stacken, med brukerens EGEN
|
|
|
|
|
|
ekte sesjonscookie** (ikke en syntetisk test): `curl` uten cookie mot
|
|
|
|
|
|
`https://teecup.teeoff.no/` ga `200` (skjemaet vises, riktig for en
|
|
|
|
|
|
anonym besøkende); samme kall MED den ekte cookien ga `307` til
|
|
|
|
|
|
`/dashboard` (riktig — sender en allerede innlogget bruker rett videre).
|
|
|
|
|
|
Ekte typesjekket produksjonsbuild kjørt FØR utrulling (build-outputet
|
|
|
|
|
|
viste selv at `/` nå er `ƒ` dynamisk i stedet for `○` statisk — bekrefter
|
|
|
|
|
|
at server-sjekken faktisk ble tatt i bruk). Rullet ut live, kun
|
|
|
|
|
|
`teecup_frontend`, ingen migrasjon, `teeoff.no` upåvirket.
|
|
|
|
|
|
**Samme runde:** brukeren stilte to oppfølgingsspørsmål om
|
|
|
|
|
|
autentisering/autorisasjon — «er flere-organisasjoner-eierskap tenkt
|
|
|
|
|
|
gjennom» (bekreftet: ja, ADR-002 fra dag én, ingen kodeendring nødvendig)
|
|
|
|
|
|
og et ønske om passord (valgfritt tillegg)+2FA. Fullt design skrevet som
|
|
|
|
|
|
**ADR-021** (passord/2FA) og **ADR-022** (dele/invitere/frasi seg
|
Update Todos
Legge til argon2-cffi, pyotp, qrcode i requirements.txt
Skrive migrasjon 012 (passord, 2FA, superadmin, org-invitasjoner)
app/auth.py: sesjonsstadier, passord-hashing, TOTP-hjelpere
app/routers/auth.py: passord-innlogging, 2FA-oppsett/verifisering
app/email.py: 2FA-kode og invitasjons-maler
app/routers/organizations.py: invitasjoner, medlemskapsstyring, superadmin-sti
Frontend: login-form passord-modus + 2FA-skjermer
Frontend: kontoinnstillinger + org-medlemsstyring-skjerm
Ekte typesjekket frontend-build
Scratch-verifisere hele auth-løpet grundig (backend)
Deploy mot ekte teecup_db/containere + oppdatere .md-filer
ADR-021 (passord/2FA) og ADR-022 (org-eierskap) er live. Kort oppsummert:
Nytt i innloggingen:
Passord (valgfritt tillegg til magic-link) — Argon2id, testet med ekte spesialtegn/mellomrom/æøå
2FA — TOTP eller e-post-engangskode, brukerens eget valg
Påkrevd 2FA for org-eiere/administratorer, valgfritt for andre
Ny /account-skjerm for å sette passord og styre 2FA
Nytt for organisasjoner:
Inviter andre på e-post til enhver rolle (eiere) eller kun medlem (administratorer)
Frasi deg eierskap selv, eller fjern andre medlemmer — med vern mot at en organisasjon står igjen uten eier
Superadmin-flagg (kun manuelt satt i databasen, aldri via API) som kan sette eierskap på hvilken som helst organisasjon
Ny /orgs/[id]/members-skjerm, lenket fra dashbordet
Fant og fikset tre reelle bugs underveis i scratch-testingen (ingen nådde produksjon) — en krasj i selve innloggingen og to tilfeller av en uendelig 2FA-løkke. Alt er nå verifisert grundig og rullet ut, eksisterende sesjoner er upåvirket.
2026-07-19 10:40:15 +02:00
|
|
|
|
eierskap + superadmin) — se ARCHITECTURE_DECISIONS.md.
|
|
|
|
|
|
- **ADR-021 (passord/2FA) + ADR-022 (org-eierskap) BYGGET OG LIVE
|
|
|
|
|
|
(2026-07-19), samme dag:** brukeren ba om begge sammen («Bygg det»,
|
|
|
|
|
|
bekreftet eksplisitt at det gjaldt begge ADR-ene i samme runde). Ny
|
|
|
|
|
|
migrasjon `012_password_2fa_and_org_invitations.sql`: `app_user.
|
|
|
|
|
|
password_hash`/`two_factor_method`/`totp_secret`/`is_super_admin`, ny
|
|
|
|
|
|
tabell `two_factor_code` (samme hash-og-utløp-mønster som
|
|
|
|
|
|
`magic_link_token`), ny tabell `organization_invitation` (RLS
|
|
|
|
|
|
org-isolert, INGEN egen klikkbar aksept-lenke — godtas automatisk ved
|
|
|
|
|
|
neste innlogging med matchende e-post), femte
|
|
|
|
|
|
`SECURITY DEFINER`-bro `accept_pending_invitations_by_email()` (etter
|
|
|
|
|
|
`public_tournament_org` 007, `link_player_by_email` 008,
|
|
|
|
|
|
`public_org_by_slug` 009, `public_tournament_by_code` 011).
|
|
|
|
|
|
**Sesjons-STADIER innført i `app/auth.py`:** en sesjonscookie er ikke
|
|
|
|
|
|
nødvendigvis en full sesjon lenger — `create_session_token()` tar nå en
|
|
|
|
|
|
`stage`-parameter (`full`/`pending_2fa`/`must_enroll_2fa`), lagt inn som
|
|
|
|
|
|
et JWT-claim. `get_current_user` avviser eksplisitt alt annet enn `full`
|
|
|
|
|
|
ELLER en ELDRE token uten stage-claim i det hele tatt (utstedt før denne
|
|
|
|
|
|
runden — behandlet som `full` for bakoverkompatibilitet, ingen
|
|
|
|
|
|
eksisterende bruker logget brått ut). To nye avhengigheter:
|
|
|
|
|
|
`get_pending_user` (kun for 2FA-verifiseringsendepunktene) og
|
|
|
|
|
|
`get_current_or_enrolling_user` (godtar BÅDE en full sesjon — frivillig
|
|
|
|
|
|
2FA-oppsett fra kontoinnstillinger — OG `must_enroll_2fa` — tvunget
|
|
|
|
|
|
oppsett rett etter innlogging — samme oppsett-logikk dekker begge
|
|
|
|
|
|
veiene). `get_current_user_optional` fikk samme stage-sjekk for
|
|
|
|
|
|
konsistens.
|
|
|
|
|
|
**Passord (Argon2id, ikke bcrypt):** bevisst valg for å unngå bcrypt sin
|
|
|
|
|
|
stille 72-byte-trunkering, siden brukeren eksplisitt ba om korrekt
|
|
|
|
|
|
håndtering av spesialtegn/mellomrom. `POST /auth/set-password`/
|
|
|
|
|
|
`/remove-password`/`/login-password` — sistnevnte svarer med IDENTISK
|
|
|
|
|
|
401 uansett om e-posten finnes, mangler passord, eller passordet er
|
|
|
|
|
|
feil (samme anti-enumerering som magic-link).
|
|
|
|
|
|
**2FA (TOTP via `pyotp` ELLER e-post-engangskode via eksisterende SMTP,
|
|
|
|
|
|
brukerens eget valg):** `POST /auth/2fa/setup/start` genererer en
|
|
|
|
|
|
TOTP-secret UTEN å lagre den (rundturer til klienten, som ekkoer den
|
|
|
|
|
|
tilbake i `/setup/confirm` — unngår en halvferdig 2FA-tilstand i
|
|
|
|
|
|
databasen hvis brukeren forlater oppsettet). QR-kode generert
|
|
|
|
|
|
server-side (`qrcode`-biblioteket + eksisterende Pillow-avhengighet,
|
|
|
|
|
|
ingen ny ekstern tjeneste). `POST /auth/2fa/verify` fullfører en
|
|
|
|
|
|
PÅGÅENDE innlogging.
|
|
|
|
|
|
**Tvungen 2FA for org-eier/admin (ADR-021 Beslutning D):**
|
|
|
|
|
|
`user_requires_2fa_enrollment()` sjekket ved HVER innlogging (ikke bare
|
|
|
|
|
|
første gang, siden en bruker kan bli eier av en NY org etter at kontoen
|
|
|
|
|
|
allerede eksisterer uten 2FA) — verifisert eksplisitt: en fersk
|
|
|
|
|
|
org-eier uten 2FA ble korrekt blokkert fra all normal tilgang (401) og
|
|
|
|
|
|
tvunget inn i oppsett-flyten før noe annet ble tilgjengelig.
|
|
|
|
|
|
**Organisasjonseierskap (ADR-022):** `POST/GET/DELETE
|
|
|
|
|
|
/orgs/{id}/invitations` (owner→enhver rolle, admin→KUN member — ellers
|
|
|
|
|
|
en privilegie-eskaleringsvei), `PATCH/DELETE /orgs/{id}/memberships/
|
|
|
|
|
|
{id}` (owner kan endre/fjerne hvem som helst; en bruker kan ALLTID
|
|
|
|
|
|
SENKE egen rolle selv — aldri heve den, ville vært selv-forfremmelse —
|
|
|
|
|
|
og alltid forlate selv), «siste eier»-vern (409 `LAST_OWNER`, `FOR
|
|
|
|
|
|
UPDATE`-lås mot race på alle eier-rader, samme TOCTOU-mønster som
|
|
|
|
|
|
ADR-011s to-lags-grense). Superadmin (`app_user.is_super_admin`, KUN
|
|
|
|
|
|
manuelt DB-tildelt — bevisst INGEN API-vei til å gi seg selv eller
|
|
|
|
|
|
andre flagget) får en parallell autorisasjonssti
|
|
|
|
|
|
(`get_superadmin_user`) som kan sette medlemskap på ENHVER org,
|
|
|
|
|
|
uavhengig av eget medlemskap — bevisst avgrenset til nøyaktig dette,
|
|
|
|
|
|
ikke generell tilgang til andres turnering-/spillerdata.
|
|
|
|
|
|
**Tre reelle bugs funnet OG fikset UNDER scratch-testing, ingen nådde
|
|
|
|
|
|
produksjon:**
|
|
|
|
|
|
1. `verify_magic_link` sendte en rå asyncpg-`UUID` (ikke streng) videre
|
|
|
|
|
|
til sesjonsutstedelse — `jwt.encode()` sin JSON-serialisering
|
|
|
|
|
|
krasjet rått (500) på selve innloggingen. Fant umiddelbart ved første
|
|
|
|
|
|
reelle innloggingstest. Rettet med en eksplisitt `str()`.
|
|
|
|
|
|
2. OG 3. Både `2fa/setup/confirm` og `2fa/verify` kalte først den delte
|
|
|
|
|
|
`_issue_login_result()`-hjelpefunksjonen (ment for PRIMÆR
|
|
|
|
|
|
autentisering) EN GANG TIL etter at 2FA nettopp var bekreftet — som
|
|
|
|
|
|
så (korrekt, men feil kontekst) at `two_factor_method` nå var satt
|
|
|
|
|
|
og krevde EN NY runde med 2FA for akkurat den samme innloggingen,
|
|
|
|
|
|
en uendelig løkke. Fant ved å faktisk fullføre hele innloggings-
|
|
|
|
|
|
syklusen med ekte genererte TOTP-koder (`pyotp` i test-scriptet),
|
|
|
|
|
|
ikke bare ved å lese koden. Rettet ved at begge endepunktene nå
|
|
|
|
|
|
utsteder en full sesjon DIREKTE etter vellykket 2FA-bekreftelse,
|
|
|
|
|
|
ikke via gjenbruk av den generelle sjekken.
|
|
|
|
|
|
**Frontend:** `login-form.tsx` fikk en tredje modus (passord, ved siden
|
|
|
|
|
|
av magic-link og ADR-020s invitasjonskode-modus). Ny delt
|
|
|
|
|
|
`two-factor-flow.tsx` (`TwoFactorVerifyForm`/`TwoFactorSetupForm`),
|
|
|
|
|
|
brukt av BÅDE `login-form.tsx` og `verify-form.tsx` siden begge
|
|
|
|
|
|
primær-autentiseringsveiene kan returnere samme 2FA-mellomtilstand. Ny
|
|
|
|
|
|
`/account`-skjerm (sett/fjern passord, aktiver/deaktiver 2FA) og ny
|
|
|
|
|
|
`/orgs/[id]/members`-skjerm (invitere, endre rolle, fjerne/forlate),
|
|
|
|
|
|
begge lenket fra dashbordets header/org-visning. `next.config.mjs` sin
|
|
|
|
|
|
`rewrites()` utvidet med `/superadmin/:path*` (ADR-016s konsekvens for
|
|
|
|
|
|
enhver ny API-prefiks, selv om ingen frontend-UI faktisk bruker den
|
|
|
|
|
|
ennå).
|
|
|
|
|
|
**Verifisert grundig mot fersk `teecup_scratch`** (001→012,
|
|
|
|
|
|
`test_isolation.sql` 12/12): full magic-link-bakoverkompatibilitet
|
|
|
|
|
|
(eksisterende flyt uendret for brukere uten 2FA), passord med ekte
|
|
|
|
|
|
spesialtegn/mellomrom/æøå satt og brukt til pålogging, full TOTP-runde
|
|
|
|
|
|
(oppsett→bekreft→logg ut→logg inn→krev 2FA→verifiser med ekte
|
|
|
|
|
|
`pyotp`-generert kode), full e-post-2FA-runde (samme mønster, feil kode
|
|
|
|
|
|
avvist, kode ikke gjenbrukbar), tvungen 2FA-registrering for ny
|
|
|
|
|
|
org-eier, invitasjon→auto-aksept ved førstegangsinnlogging,
|
|
|
|
|
|
rolle-eskalering-forsøk avvist på tre distinkte måter (medlem kan ikke
|
|
|
|
|
|
invitere, admin kan ikke gi eierskap, bruker kan ikke forfremme seg
|
|
|
|
|
|
selv), siste-eier-vern (både PATCH og DELETE), duplikat-invitasjon
|
|
|
|
|
|
avvist, superadmin-sti fungerer/avvises riktig i begge retninger. Ekte
|
|
|
|
|
|
typesjekket produksjonsbuild av frontend (alle nye ruter listet).
|
|
|
|
|
|
**Rullet ut mot ekte systemer 2026-07-19**, bruker bekreftet eksplisitt:
|
|
|
|
|
|
migrasjon 012 kjørt mot ekte `teecup_db`, `test_isolation.sql` fortsatt
|
|
|
|
|
|
12/12, begge containere (`teecup_api`, `teecup_frontend`) redeployet og
|
|
|
|
|
|
bekreftet ren oppstart, `/health` og `/dashboard` fortsatt 200 (eksisterende
|
|
|
|
|
|
sesjoner uendret av stage-bakoverkompatibiliteten), `/auth/login-password`
|
|
|
|
|
|
bekreftet nåbart over ekte https, `teeoff.no` upåvirket.
|
|
|
|
|
|
**ADR-021 og ADR-022 er dermed begge helt ferdig** — backend + frontend.
|
|
|
|
|
|
Bevisst utenfor omfang: dedikert superadmin-UI (brukes via API av en
|
|
|
|
|
|
betrodd operatør), SMS som 2FA-metode.
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
|
|
2026-07-19 11:46:33 +02:00
|
|
|
|
- **Program-skjerm: tydeligere klikk-hint på øktkort (2026-07-19):** brukeren
|
|
|
|
|
|
påpekte at ingenting i grensesnittet indikerte at et øktkort er klikkbart
|
|
|
|
|
|
inn til blind draw-skjermen. Lagt til en synlig "Sett opp flights og lås
|
|
|
|
|
|
oppstilling →"-rad nederst i hvert kort (`components/tournament-program.tsx`).
|
|
|
|
|
|
Ren frontend-endring, ingen backend-rørt.
|
|
|
|
|
|
- **Brukerroller: kaptein som reell autorisasjon, deltaker-avgrenset scoring
|
|
|
|
|
|
(2026-07-19, ADR-023):** direkte oppfølging av det lenge åpne
|
|
|
|
|
|
"Brukerroller"-punktet i FEATURE_BACKLOG.md. Fire beslutninger avklart
|
|
|
|
|
|
eksplisitt med bruker (AskUserQuestion) før bygging — alle anbefalte valg.
|
|
|
|
|
|
**Bygget:** `app/team_authz.py` skrevet om — `user_is_team_captain`
|
|
|
|
|
|
(erstatter `user_may_act_for_team`) krever `is_captain=true` på
|
|
|
|
|
|
`team_roster` (eller org-eier/admin) for å legge til/fjerne deltakere og
|
|
|
|
|
|
låse et lag (`matches.py`); ny `user_is_match_participant` krever en ekte
|
|
|
|
|
|
`match_participant`-rad for brukeren i AKKURAT den matchen (valgfritt
|
|
|
|
|
|
side-spesifikk via `team_side`) for å føre/korrigere score (`scoring.py`)
|
|
|
|
|
|
— uavhengig av kapteinmerket. Nye feilkoder `NOT_TEAM_CAPTAIN` og
|
|
|
|
|
|
`NOT_MATCH_PARTICIPANT` (erstatter `NOT_ROSTERED_ON_TEAM` på disse fem
|
|
|
|
|
|
stedene). `app/routers/tournaments.py` sin `PATCH`/`POST .../roster`
|
|
|
|
|
|
håndhever nå "kun én kaptein per lag" (fjerner automatisk forrige
|
|
|
|
|
|
kapteins merke i samme transaksjon).
|
|
|
|
|
|
**Reelt funn FØR utrulling, ikke antatt:** sjekket (kun lesing, superbruker
|
|
|
|
|
|
mot ekte `teecup_db`) om noen eksisterende lag ville blitt låst ute av en
|
|
|
|
|
|
ren kaptein-only-regel — "De Unge" i "De Gamle er Eldst" har i dag 0 av 2
|
|
|
|
|
|
roster-rader merket kaptein. Designet derfor en bevisst fallback i
|
|
|
|
|
|
`user_is_team_captain`: har laget INGEN utpekt kaptein ennå, godtas enhver
|
|
|
|
|
|
rostret spiller i stedet for å låse laget helt ute. Ingen lag hadde flere
|
|
|
|
|
|
kapteiner, så "kun én kaptein"-håndhevelsen krevde ingen data-opprydning.
|
|
|
|
|
|
**Reell bug funnet OG fikset UNDER scratch-testing:** `user_is_match_
|
|
|
|
|
|
participant` sin SQL sammenlignet `mp.team_side` (enum-kolonne) direkte
|
|
|
|
|
|
mot en tekst-parameter uten cast når `team_side=None` (hole_result-modus)
|
|
|
|
|
|
— ga en rå 500 (`UndefinedFunctionError: operator does not exist: team_side
|
|
|
|
|
|
= text`). Rettet med et eksplisitt `mp.team_side::text = $3`.
|
|
|
|
|
|
**Verifisert grundig mot fersk `teecup_scratch`** (isolert
|
|
|
|
|
|
`teecup_app_scratch`-rolle, isolert scratch-MinIO, engangs API-container):
|
|
|
|
|
|
et 15-punkts Python/httpx-testskript som simulerte fire innloggede
|
|
|
|
|
|
brukere (organisator + tre rostrede spillere på to lag) gjennom hele
|
|
|
|
|
|
syklusen — lag uten kaptein tillater enhver rostret (fallback bekreftet),
|
|
|
|
|
|
kaptein utpekt fjerner andre rostredes rettighet, "kun én kaptein" bekreftet
|
|
|
|
|
|
(ny kaptein avsetter automatisk forrige), org-admin fungerer uendret
|
|
|
|
|
|
uavhengig av kaptein, og scoring (`hole_result`-modus) bekreftet begrenset
|
|
|
|
|
|
til faktiske matchdeltakere (en kaptein som IKKE selv spiller matchen ble
|
|
|
|
|
|
korrekt avvist med `NOT_MATCH_PARTICIPANT`, mens en faktisk deltaker og
|
|
|
|
|
|
org-admin begge fikk føre score). Måtte også oppdage og legge til et
|
|
|
|
|
|
forutsetning-steg underveis: `get_authorized_org` krever
|
|
|
|
|
|
`organization_membership` for ALLE org-scopede endepunkter uansett — en
|
|
|
|
|
|
rostret spiller må derfor også være invitert som org-medlem (minimum
|
|
|
|
|
|
'member', ADR-022s invitasjonsflyt) for i det hele tatt å nå
|
|
|
|
|
|
team_authz-vurderingen; ikke en bug, men en forutsetning testskriptet
|
|
|
|
|
|
først manglet. `test_isolation.sql` 12/12 uendret (ingen skjemaendring).
|
|
|
|
|
|
Ekte typesjekket produksjonsbuild av frontend (øktkort-hintet fra samme
|
|
|
|
|
|
runde) kjørt og bekreftet, alle 11 ruter listet.
|
|
|
|
|
|
**Rullet ut live 2026-07-19**, bruker bekreftet eksplisitt: ingen
|
|
|
|
|
|
migrasjon, `docker compose up -d --build teecup_api teecup_frontend`,
|
|
|
|
|
|
begge containere boot-et rent (`Application startup complete`, Next.js
|
|
|
|
|
|
`Ready`), `/health` og `/dashboard` → 200, `teeoff.no` upåvirket.
|
|
|
|
|
|
|
2026-07-19 21:36:33 +02:00
|
|
|
|
- **Walkover/konsesjon LIVE (2026-07-19, ADR-024):** direkte oppfølging av
|
|
|
|
|
|
Brukerroller-runden samme dag — brukeren ba eksplisitt om å ta fatt på
|
|
|
|
|
|
dette som naturlig neste steg. Løser det lenge kjente hullet: en side som
|
|
|
|
|
|
aldri stiller nok spillere fikk aldri beregnet handicap og matchen kunne
|
|
|
|
|
|
derfor aldri avgjøres — hang uendelig.
|
|
|
|
|
|
Fire beslutninger avklart eksplisitt med bruker (AskUserQuestion) før
|
|
|
|
|
|
bygging — tre anbefalte valg, ett (omfang: match+turnering-nivå samtidig,
|
|
|
|
|
|
ikke bare match) valgt utover anbefalingen.
|
|
|
|
|
|
**Bygget:** ny `apply_concession`-hjelpefunksjon i `app/routers/
|
|
|
|
|
|
scoring.py` (skriver til de samme fire kolonnene som
|
|
|
|
|
|
`recompute_and_cache_match_state` -- `status_text`/`points_side_a/b`/
|
|
|
|
|
|
`leading_side` -- men direkte, ikke utledet fra hull). Ny
|
|
|
|
|
|
`POST /orgs/{id}/matches/{id}/concede` (kun kaptein for det TAPENDE laget,
|
|
|
|
|
|
speiler ekte golf-etikette -- du gir bort DITT tap, krever ikke seier på
|
|
|
|
|
|
motstanderens vegne -- eller org-admin, gjenbruker `user_is_team_captain`
|
|
|
|
|
|
fra ADR-023 uendret). Ny `POST /orgs/{id}/tournaments/{id}/concede`
|
|
|
|
|
|
(`app/routers/tournaments.py`) som gir opp ALLE ikke-avgjorte matcher
|
|
|
|
|
|
laget har i turneringen i én operasjon -- v1s to-lags-grense (ADR-011)
|
|
|
|
|
|
gjør dette trivielt (bare én motstander uansett), `FOR UPDATE`-låser alle
|
|
|
|
|
|
berørte match-rader (samme race-vern som ellers). Kan erklæres uansett
|
|
|
|
|
|
hvor mange hull som allerede er registrert (match-play teller kun
|
|
|
|
|
|
seier/tap/delt for poeng, ikke marginen) -- allerede registrerte hull i
|
|
|
|
|
|
`hole_score`/`match_hole_result` forblir urørt, kun matchens
|
|
|
|
|
|
avgjørelses-felt endres.
|
|
|
|
|
|
**Frontend:** `session-scorecard.tsx` fikk en kollapsbar
|
|
|
|
|
|
"Gi opp matchen (walkover)"-seksjon (to knapper, én per lag, med
|
|
|
|
|
|
bekreftelsessteg) synlig når matchen ikke er avgjort.
|
|
|
|
|
|
`tournament-detail.tsx` sin `TeamPanel` fikk en tilsvarende "Gi opp resten
|
|
|
|
|
|
av turneringen for laget"-knapp nederst i hvert lagkort. Ingen
|
|
|
|
|
|
klientside-forhåndsfiltrering på kapteinstatus noe sted -- begge knappene
|
|
|
|
|
|
vises alltid, en 403 fra backend vises bare som vanlig feiltekst (samme
|
|
|
|
|
|
mønster som resten av appen).
|
|
|
|
|
|
**Verifisert grundig mot fersk `teecup_scratch`** (isolert
|
|
|
|
|
|
`teecup_app_scratch`-rolle, isolert scratch-MinIO, engangs
|
|
|
|
|
|
API-container): to separate 15-punkts Python/httpx-testløp. Match-nivå:
|
|
|
|
|
|
vinnende lags kaptein NEKTES å konsedere på vegne av det tapende laget
|
|
|
|
|
|
(kan ikke kreve seier på andres vegne), tapende lags kaptein FÅR, allerede
|
|
|
|
|
|
avgjort match avvist 409, fremmed team_id avvist 400, konsesjon ETTER at
|
|
|
|
|
|
ett hull allerede er registrert bekreftet å fungere OG bekreftet at det
|
|
|
|
|
|
registrerte hull-resultatet forblir synlig i scorekortet etterpå (ikke
|
|
|
|
|
|
overskrevet), org-admin FÅR konsedere direkte uavhengig av kapteinmerke.
|
|
|
|
|
|
Turnering-nivå: feil lags kaptein nektes, riktig kaptein FÅR gi opp
|
|
|
|
|
|
resten (kun de faktisk ikke-avgjorte matchene telles -- allerede avgjorte
|
|
|
|
|
|
matcher fra match-nivå-testene i samme løp ble korrekt hoppet over),
|
|
|
|
|
|
gjentatt kall er trygt (0 nye, idempotent i praksis), ukjent team_id gir
|
|
|
|
|
|
404. `test_isolation.sql` 12/12 uendret (ingen skjemaendring). Ekte
|
|
|
|
|
|
typesjekket produksjonsbuild av frontend kjørt og bekreftet, alle 11
|
|
|
|
|
|
ruter listet.
|
|
|
|
|
|
**Rullet ut live 2026-07-19**, bruker bekreftet eksplisitt: ingen
|
|
|
|
|
|
migrasjon, `docker compose up -d --build teecup_api teecup_frontend`,
|
|
|
|
|
|
begge containere boot-et rent, `/health` og `/dashboard` → 200,
|
|
|
|
|
|
`teeoff.no` upåvirket.
|
|
|
|
|
|
|
2026-07-19 21:51:05 +02:00
|
|
|
|
- **Turnering-status via API, SCRATCH-VERIFISERT (2026-07-19):** brukeren
|
|
|
|
|
|
valgte dette som neste steg etter walkover/konsesjon-runden (fikk velge
|
|
|
|
|
|
mellom denne lille opprydningen og å starte Kommunikasjon-runden). `status`
|
|
|
|
|
|
(draft/active/completed/archived) har ligget i skjemaet siden migrasjon
|
|
|
|
|
|
001, men INGEN endepunkt kunne endre det — kun `INSERT`-defaulten `'draft'`
|
|
|
|
|
|
fra `create_tournament`.
|
|
|
|
|
|
**Bygget:** lagt til i `TournamentUpdate` (`app/routers/tournaments.py`),
|
|
|
|
|
|
settes via det eksisterende generiske `PATCH /orgs/{id}/tournaments/{id}`
|
|
|
|
|
|
(`exclude_unset`-mønsteret, ekte PATCH-semantikk uendret).
|
|
|
|
|
|
**Reelt funn UNDER scratch-testing, ikke antatt riktig på forhånd:**
|
|
|
|
|
|
`status` er -- ulikt `visibility` (som er ren `text`+`CHECK`) -- en EKTE
|
|
|
|
|
|
Postgres ENUM-type (`tournament_status`). Den generiske
|
|
|
|
|
|
`set_clauses`-byggeren (`f"{key} = ${i}"`) hadde derfor trengt et
|
|
|
|
|
|
eksplisitt cast for at asyncpg sin ukjent-typede parameter skulle løses
|
|
|
|
|
|
riktig mot en enum-kolonne -- lagt til en spesialsjekk (`::tournament_status`
|
|
|
|
|
|
kun for `status`-nøkkelen) FØR jeg antok mekanismen "bare fungerer" fordi
|
|
|
|
|
|
den gjør det for de andre feltene.
|
|
|
|
|
|
Bevisst INGEN tilstandsmaskin/overgangsregler bygget (kan f.eks. gå fra
|
|
|
|
|
|
`completed` tilbake til `draft` fritt) -- samme tillitsnivå som resten av
|
|
|
|
|
|
appen, ikke etterspurt.
|
|
|
|
|
|
**Frontend:** `tournament-status-badge.tsx` fikk en ny redigerbar
|
|
|
|
|
|
`TournamentStatusPicker` (dropdown over de fire verdiene, optimistisk
|
|
|
|
|
|
UI-oppdatering med rollback ved feil), koblet inn i `tournament-detail.tsx`
|
|
|
|
|
|
sin header ved siden av invitasjonskode-chipen. Den eksisterende
|
|
|
|
|
|
skrivebeskyttede `TournamentStatusBadge` (dashbordets kortliste) urørt.
|
|
|
|
|
|
**Verifisert i scratch:** ny turnering får riktig default `draft`, PATCH
|
|
|
|
|
|
til `active` bekreftet enum-castet faktisk løser problemet, PATCH med
|
|
|
|
|
|
status+et annet felt samtidig fungerer, PATCH UTEN status-felt lar
|
|
|
|
|
|
verdien stå urørt (regresjon på eksisterende PATCH-semantikk), ugyldig
|
|
|
|
|
|
status-verdi avvist med 422 (Pydantic-mønster), `GET`-listen viser samme
|
|
|
|
|
|
verdi etterpå. `test_isolation.sql` 12/12 uendret (ingen skjemaendring).
|
|
|
|
|
|
Ekte typesjekket produksjonsbuild kjørt og bekreftet.
|
|
|
|
|
|
**Rullet ut live 2026-07-19**, bruker bekreftet eksplisitt: ingen
|
|
|
|
|
|
migrasjon, `docker compose up -d --build teecup_api teecup_frontend`,
|
|
|
|
|
|
begge containere boot-et rent, `/health` og `/dashboard` → 200,
|
|
|
|
|
|
`teeoff.no` upåvirket.
|
|
|
|
|
|
|
Kommunikasjon er live — det største enkeltløftet i prosjektet så langt:
Lag-chat («det hemmelige rommet»), tilgjengelig via en ny chat-knapp på hvert lagkort i lag/spillere-skjermen — ekte privat, org-eier/admin har ikke tilgang (verifisert helt ned til selve WebSocket-håndtrykket, ikke bare REST).
Offentlig runde-feed («Banter Board»), ny seksjon nederst på turneringens offentlige side — gjenbruker eksisterende synlighetsnivåer, men posting krever innlogging + tilknytning til turneringen/organisasjonen.
Bilder i begge, sanntid via WebSockets i begge.
Underveis: fant at Next.js ikke proxyer WebSocket-oppgraderinger pålitelig, løst med en egen Caddy-rute rett til API-et (samme mønster som media-ruten fra MinIO-runden). Alt scratch-verifisert grundig (20 automatiserte sjekker inkl. faktisk sanntidsmottak over en åpen WebSocket, ikke bare REST-svar), og bekreftet på ekte produksjon med et reelt wss://-håndtrykk over https.
Viktig å huske til neste økt: Caddy-endringen ligger uncommitted i det separate /opt/teeoff-repoet — samme fallgruve som tidligere Caddy-runder.
Alt er dokumentert i .md-filene. Naturlig neste kandidat er tilskuer-rollen (som Kommunikasjon-arbeidet nå gjør mulig å definere skikkelig), men si fra hva du vil prioritere.
2026-07-19 22:33:13 +02:00
|
|
|
|
- **Kommunikasjon LIVE (2026-07-19, ADR-025) — det største enkeltløftet i
|
|
|
|
|
|
prosjektet så langt:** lag-intern chat («det hemmelige rommet») + offentlig
|
|
|
|
|
|
runde-feed («Banter Board»), begge med bilder og ekte WebSocket-sanntid,
|
|
|
|
|
|
bygget i samme runde. Fire hovedbeslutninger avklart eksplisitt med bruker
|
|
|
|
|
|
(AskUserQuestion) før bygging — brukeren valgte den mest ambisiøse
|
|
|
|
|
|
kombinasjonen på alle fire (begge deler nå, WebSockets fremfor polling,
|
|
|
|
|
|
ekte privat chat, bilder fra start).
|
|
|
|
|
|
**Datamodell:** ny migrasjon `013_messaging.sql` — delt `message`-tabell
|
|
|
|
|
|
med `scope`-diskriminator (`team`/`tournament_feed`) i stedet for to
|
|
|
|
|
|
separate tabeller, RLS org_isolation som ellers. `author_display_name`
|
|
|
|
|
|
FRYSES ved skrivetidspunkt (samme prinsipp som handicap-snapshot,
|
|
|
|
|
|
ADR-007) — spillerens `player.display_name` i org-en hvis den finnes,
|
|
|
|
|
|
ellers e-postens lokaldel (dekker org-ansatte uten egen spillerprofil).
|
|
|
|
|
|
**Lag-chat er BEVISST ekte privat** — ny `user_is_rostered_on_team` i
|
|
|
|
|
|
`app/team_authz.py`, med VILJE uten org-admin-fallback, ulikt de to andre
|
|
|
|
|
|
funksjonene i samme fil (`user_is_team_captain`/`user_is_match_participant`,
|
|
|
|
|
|
ADR-023, som begge har et slikt unntak). Første sted i hele appen der
|
|
|
|
|
|
org-eier/admin er strukturelt utestengt fra noe.
|
|
|
|
|
|
**Offentlig feed:** LESING gjenbruker `registration.py` sitt eksisterende
|
|
|
|
|
|
trenivå-visibility-mønster (ADR-018) helt uendret, inkl. anonym tilgang.
|
|
|
|
|
|
POSTING er strengere enn lesing — krever ekte innlogging OG org-
|
|
|
|
|
|
medlemskap/faktisk deltakelse, selv på en `public`-synlig turnering (en
|
|
|
|
|
|
helt urelatert innlogget bruker skal ikke kunne poste på en fremmed
|
|
|
|
|
|
offentlig side). Moderering: forfatteren selv ELLER org-eier/admin kan
|
|
|
|
|
|
slette et feed-innlegg (motsatt av lag-chatten, som ikke har noen
|
|
|
|
|
|
ekstern moderator).
|
|
|
|
|
|
**`registration.py` sine fire interne hjelpefunksjoner gjort delt**
|
|
|
|
|
|
(fjernet ledende understrek — samme "gjort delt for gjenbruk"-mønster som
|
|
|
|
|
|
tidligere runder): `resolve_org`, `is_participant`, `code_matches`,
|
|
|
|
|
|
`check_visibility`. `team_authz.py` sin `_is_org_admin` likeens →
|
|
|
|
|
|
`is_org_admin`. Ingen atferdsendring, kun navn, for at `messaging.py`
|
|
|
|
|
|
skulle kunne gjenbruke dem uendret i stedet for å duplisere logikk.
|
|
|
|
|
|
**WebSockets, ikke polling:** in-memory tilkoblingsregister PER PROSESS i
|
|
|
|
|
|
`app/routers/messaging.py` — trygt med dagens ene `teecup_api`-container,
|
|
|
|
|
|
men deles IKKE på tvers av flere prosesser/containere (samme klasse
|
|
|
|
|
|
begrensning som den allerede aksepterte in-memory-cachen, se ARCHITECTURE_
|
|
|
|
|
|
DECISIONS.md "Åpne spørsmål"). Ny `get_current_user_from_websocket` i
|
|
|
|
|
|
`app/auth.py` — WS-ruter kan ikke bruke `get_current_user`/
|
|
|
|
|
|
`get_authorized_org` direkte via `Depends()` (de er `Request`-typet, ingen
|
|
|
|
|
|
ekte HTTP Request finnes i en WS-scope), derfor en bevisst minimal, egen
|
|
|
|
|
|
kopi av samme cookie-dekode-/oppslagslogikk.
|
|
|
|
|
|
**Reell infrastrukturoppdagelse FØR noe ble forsøkt, ikke i etterkant:**
|
|
|
|
|
|
Next.js sin `rewrites()` proxyer ikke WebSocket-oppgraderinger pålitelig i
|
|
|
|
|
|
"standalone"-modus — løst likt som MinIO-media-ruten (ADR-018): en egen
|
|
|
|
|
|
Caddy-rute (`handle /ws/* { reverse_proxy teecup_api:8000 }`) rett til
|
|
|
|
|
|
API-et, forbi Next.js/`teecup_frontend` helt. Caddyfile ligger i det
|
|
|
|
|
|
SEPARATE `/opt/teeoff`-repoet — samme stale-bind-mount-inode-oppførsel som
|
|
|
|
|
|
ALLE tidligere Caddyfile-runder (graceful reload plukker ikke opp
|
|
|
|
|
|
endringen), løst likt: full `docker restart teeoff_caddy`, brukeren
|
|
|
|
|
|
bekreftet eksplisitt på forhånd, noen sekunders nedetid for `teeoff.no`.
|
|
|
|
|
|
**Verifisert grundig mot fersk `teecup_scratch`** (isolert
|
|
|
|
|
|
`teecup_app_scratch`-rolle, isolert scratch-MinIO, engangs API-container
|
|
|
|
|
|
med `websockets`-Python-biblioteket installert kun for testen): et
|
|
|
|
|
|
20-punkts asyncio/httpx/websockets-testskript som dekket BEGGE
|
|
|
|
|
|
meldingstyper ende-til-ende. Kritiske personvern-/sanntid-funn, alle
|
|
|
|
|
|
bekreftet med ekte tilkoblinger (ikke bare REST):
|
|
|
|
|
|
- Rostret spiller på lag A FÅR lese/skrive lag A sin chat; rostret spiller
|
|
|
|
|
|
på lag B NEKTES; **organisatoren (org-eier) NEKTES OGSÅ** — bekreftet
|
|
|
|
|
|
BÅDE over REST (`GET`) og over selve WebSocket-håndtrykket (avvist med
|
|
|
|
|
|
lukkekode 4403 før `accept()` i det hele tatt kalles).
|
|
|
|
|
|
- Sanntid bekreftet reelt: A1 koblet til lag A sin chat-socket, A2 sendte
|
|
|
|
|
|
en melding over vanlig REST, A1 mottok den umiddelbart over den åpne
|
|
|
|
|
|
WebSocket-tilkoblingen (ikke bare at REST-svaret så riktig ut).
|
|
|
|
|
|
- Bildeopplasting i chat bekreftet (ekte AVIF-konvertert `image_url`
|
|
|
|
|
|
returnert).
|
|
|
|
|
|
- Offentlig feed: anonym NEKTES å poste (401), en tilfeldig INNLOGGET men
|
|
|
|
|
|
uvedkommende bruker NEKTES (403 `NOT_A_PARTICIPANT`), org-medlem FÅR,
|
|
|
|
|
|
en faktisk deltaker (rostret, IKKE org-medlem) FÅR — beviser
|
|
|
|
|
|
`is_participant`-veien fungerer uavhengig av `is_member`-veien. Anonym
|
|
|
|
|
|
WebSocket-tilkobling til en `public`-synlig turnerings feed FÅR lov og
|
|
|
|
|
|
mottar sanntidsoppdateringer.
|
|
|
|
|
|
- Moderering bekreftet: forfatter sletter eget innlegg, org-admin sletter
|
|
|
|
|
|
ANDRES innlegg (feeden), uvedkommende NEKTES å slette andres innlegg.
|
|
|
|
|
|
`test_isolation.sql` 12/12 uendret (additiv migrasjon). Ekte typesjekket
|
|
|
|
|
|
produksjonsbuild av frontend kjørt og bekreftet, inkl. den nye
|
|
|
|
|
|
`/tournaments/[id]/teams/[teamId]/chat`-ruten.
|
|
|
|
|
|
**Frontend:** ny `components/team-chat.tsx` (meldingsliste med egen/andres-
|
|
|
|
|
|
styling, bildeopplasting, sanntid via nettleserens native `WebSocket`,
|
|
|
|
|
|
slett-egen-melding), lenket fra en ny chat-ikon-knapp i `tournament-
|
|
|
|
|
|
detail.tsx` sin `TeamPanel`. Ny seksjon `TournamentFeed` i
|
|
|
|
|
|
`components/public-tournament.tsx` (nederst på den offentlige
|
|
|
|
|
|
turneringssiden) — viser 401/403-svar fra posting som forklarende
|
|
|
|
|
|
inline-tekst ("logg inn for å poste" / "du må være medlem/deltaker") i
|
|
|
|
|
|
stedet for en generisk feilmelding.
|
|
|
|
|
|
**Rullet ut live 2026-07-19**, bruker bekreftet eksplisitt for alle tre
|
|
|
|
|
|
stegene (migrasjon, containere, Caddy-restart): migrasjon 013 kjørt mot
|
|
|
|
|
|
ekte `teecup_db`, `test_isolation.sql` fortsatt 12/12, begge containere
|
|
|
|
|
|
boot-et rent, Caddy validert (`caddy validate` — "Valid configuration")
|
|
|
|
|
|
FØR restart, restarten ren (ingen feil i loggen). Verifisert grundig
|
|
|
|
|
|
etterpå: `/health`/`dashboard` → 200, `teeoff.no` → 200, et ekte
|
|
|
|
|
|
`wss://`-håndtrykk over produksjons-https bekreftet å nå helt frem til
|
|
|
|
|
|
applikasjonslaget (testet mot en ukjent turnering-id — ingen ekte data
|
|
|
|
|
|
berørt — ga korrekt 404 fra selve WS-ruten, ikke en Caddy/Next.js-feil),
|
|
|
|
|
|
og et ren-HTTP-kall mot en `/ws/*`-sti bekreftet å returnere FastAPI sin
|
|
|
|
|
|
egen JSON-404 (`{"detail":"Not Found"}`) og ikke Next.js sin HTML-404 —
|
|
|
|
|
|
beviser Caddy-ruten faktisk treffer `teecup_api`, ikke `teecup_frontend`.
|
|
|
|
|
|
|
2026-07-16 08:21:57 +02:00
|
|
|
|
Neste steg:
|
2026-07-18 18:22:10 +02:00
|
|
|
|
1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
|
Kommunikasjon er live — det største enkeltløftet i prosjektet så langt:
Lag-chat («det hemmelige rommet»), tilgjengelig via en ny chat-knapp på hvert lagkort i lag/spillere-skjermen — ekte privat, org-eier/admin har ikke tilgang (verifisert helt ned til selve WebSocket-håndtrykket, ikke bare REST).
Offentlig runde-feed («Banter Board»), ny seksjon nederst på turneringens offentlige side — gjenbruker eksisterende synlighetsnivåer, men posting krever innlogging + tilknytning til turneringen/organisasjonen.
Bilder i begge, sanntid via WebSockets i begge.
Underveis: fant at Next.js ikke proxyer WebSocket-oppgraderinger pålitelig, løst med en egen Caddy-rute rett til API-et (samme mønster som media-ruten fra MinIO-runden). Alt scratch-verifisert grundig (20 automatiserte sjekker inkl. faktisk sanntidsmottak over en åpen WebSocket, ikke bare REST-svar), og bekreftet på ekte produksjon med et reelt wss://-håndtrykk over https.
Viktig å huske til neste økt: Caddy-endringen ligger uncommitted i det separate /opt/teeoff-repoet — samme fallgruve som tidligere Caddy-runder.
Alt er dokumentert i .md-filene. Naturlig neste kandidat er tilskuer-rollen (som Kommunikasjon-arbeidet nå gjør mulig å definere skikkelig), men si fra hva du vil prioritere.
2026-07-19 22:33:13 +02:00
|
|
|
|
2. Fortsatt åpne beslutninger fra FEATURE_BACKLOG.md: tilskuer-rolle (bevisst
|
|
|
|
|
|
utsatt til nå at Kommunikasjon er ferdig — bør tas fatt på snart, samme
|
|
|
|
|
|
begrunnelse som før: definer sammen med feed-synligheten, som nå finnes),
|
|
|
|
|
|
kode-regenerering for ADR-020, korrigering-godkjenning fra motpart, live
|
|
|
|
|
|
leaderboard (samme WebSocket-mekanisme som meldinger kan gjenbrukes, ikke
|
|
|
|
|
|
koblet til ennå), video/1-til-1-meldinger (bevisst utsatt i ADR-025).
|
|
|
|
|
|
3. Flere turneringsformater utover Ryder Cup (Københavner/High-low-high/
|
|
|
|
|
|
Robbins/Try all, notert 2026-07-19) — ingen ADR-runde startet ennå.
|
|
|
|
|
|
4. **Merk for neste økt:** `deploy/Caddyfile`-endringen (ny `/ws/*`-rute)
|
|
|
|
|
|
ligger uncommitted i det SEPARATE `/opt/teeoff`-repoet, ikke i
|
|
|
|
|
|
`teecup`-repoet — samme fallgruve som ADR-016-runden sin Caddy-endring,
|
|
|
|
|
|
lett å glemme siden denne økten ellers kun har jobbet i `/opt/teecup`.
|
|
|
|
|
|
5. Ved fremtidige nye V0-skjermer/-design i V0 (fortsett i samme prosjekt),
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
|
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.
|