diff --git a/054_personal_course_provenance.sql b/054_personal_course_provenance.sql new file mode 100644 index 0000000..85b8960 --- /dev/null +++ b/054_personal_course_provenance.sql @@ -0,0 +1,23 @@ +-- Bane-mal-bibliotek, del II (2026-08-04) -- brukeren oppdaget at +-- turneringsmodulen manglet TeeOff-import for individuelle turneringer OG at +-- "opprett manuell bane" der bare var et bart navnefelt (ingen hull/utslag- +-- skjema fantes). Løsningen: la BEGGE moduler (turnering + single-runde) +-- tilby "bruk en eksisterende bane som mal" -- enten fra TeeOff eller fra en +-- annen bruker/organisasjons offentlige custom-bane -- som forhåndsutfyller +-- et redigerbart hull/utslag-skjema før lagring. +-- +-- personal_course (020_personal_rounds.sql) er, til tross for navnet, +-- ALLEREDE et globalt, plattform-omfattende bibliotek -- GET /personal- +-- courses søker på tvers av ALLE brukeres baner uten eier- eller org- +-- filtrering (rounds.py:142-149). Bekreftet med bruker: dette biblioteket +-- er nettopp det "offentlige" stedet begge moduler skal dele -- ingen ny +-- tabell trengs, kun denne ene kolonnen for opphav/attribusjon når en bane +-- er en redigert kopi ("fork") av en annen. +-- +-- Eierskaps-håndhevelse for de nye endre-/dupliser-/slette-endepunktene +-- (rounds.py) skjer i APPLIKASJONSLAGET (created_by_user_id == +-- innlogget bruker), IKKE via RLS -- personal_course har aldri vært +-- org-scopet eller RLS-dekket (bevisst, siden opprettelsen i migrasjon 020), +-- og det er ingen grunn til å endre det prinsippet nå. +ALTER TABLE personal_course + ADD COLUMN forked_from_id uuid REFERENCES personal_course (id) ON DELETE SET NULL; diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index 39f7198..f88ac71 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -3899,6 +3899,85 @@ for-tall-bekreftet leaderboard-rangering per klasse i ekte nettleser). --- +## ADR-042: Delt, plattform-omfattende bane-mal-bibliotek (`personal_course`) + +**Kontekst:** Brukeren oppdaget at turneringsmodulens "opprett manuell +bane" i praksis var ubrukelig (kun et navnefelt, ingen vei til hull/ +utslag), og ba om at BÅDE turneringsmodulen og single-runde-modulen skal +tilby "bruk en eksisterende bane (TeeOff eller andres custom-bane) som +mal" ved manuell baneoppretting. Et oppfølgingsspørsmål presiserte at +disse custom-banene "bør lagres, og de må være offentlige" — synlige og +gjenbrukbare av andre. + +**Beslutning A — Gjenbruk `personal_course`, ikke en ny tabell.** +`personal_course` (020_personal_rounds.sql, opprinnelig bygget for +frittstående runder) er, til tross for navnet og til tross for at ingen +tidligere runde eksplisitt utnyttet det, ALLEREDE en global, +plattform-omfattende katalog — `GET /personal-courses` søker på tvers av +ALLE brukeres baner uten eier- eller organisasjonsfiltrering, siden +tabellen (bevisst, fra migrasjon 020) aldri har vært RLS-/org-scopet. +Fremfor å bygge en ny `community_course`-tabell (vurdert og forkastet) +gjenbrukes denne eksisterende, allerede-globale tabellen direkte som det +delte mal-biblioteket — eneste tilføyelse er én kolonne, +`forked_from_id` (migrasjon 054, selvreferende FK, `ON DELETE SET NULL`) +for proveniens/attribusjon. Dette er også den ENESTE farbare veien for +at single-runde-modulen (som ikke har noe organisasjons-begrep i det +hele tatt) kan dele samme bibliotek som org-turneringsmodulen. + +**Beslutning B — "Offentlig" betyr hele plattformen, ikke bare egen +organisasjon.** Bekreftet eksplisitt med bruker (`AskUserQuestion`) etter +at jeg flagget spenningen: skal en publisert custom-bane være synlig for +ALLE organisasjoner + alle frittstående brukere, eller kun innad i én +organisasjon? Svaret var plattform-omfattende — organisasjonsgrensen +gjelder domenedata (turneringer, spillere, resultater), ikke dette +delte, lavsensitive bane-referansebiblioteket. En org-`course`-rad +(brukt til faktisk spill i en turnering) forblir like fullt org-scopet +og RLS-beskyttet som før — publisering til `personal_course` skjer som +en SEPARAT, samtidig innsetting i samme transaksjon (`courses.py`), ikke +en endring av `course`-tabellens egen skoping. + +**Beslutning C — Eierskap håndheves i applikasjonslaget, ikke RLS.** +Siden `personal_course` bevisst aldri har vært org-scopet, finnes det +ingen `app.current_org`-kontekst å håndheve mot. De nye +endre-/dupliser-/slette-endepunktene (`rounds.py`) sjekker eksplisitt +`created_by_user_id == innlogget bruker` i handler-koden — samme +`plain_connection()`-uten-RLS-mønster som resten av `personal_course`/ +`round`-familien allerede bruker (ADR-033 Beslutning A). + +**Beslutning D — Rediger forker automatisk hvis du ikke eier raden; +Dupliser er en egen, eksplisitt handling.** `PATCH /personal-courses/{id}`: +eier den innloggede brukeren raden, oppdateres den i sted; eier +brukeren IKKE raden, opprettes automatisk en NY rad (eid av innlogget +bruker, `forked_from_id` satt til originalen) — originalen selv røres +aldri. Ett endepunkt dekker begge casene uten at frontend selv må +forgrene på eierskap. `POST .../duplicate` er en SEPARAT, eksplisitt +handling tilgjengelig uansett eierskap på kilden — dekker f.eks. å lage +en variant av DIN EGEN bane uten å miste originalen, noe fork-ved- +redigering alene ikke gjør (den trigges kun når raden IKKE er din). +Bekreftet eksplisitt med bruker at begge mekanismene skulle beholdes +side om side. + +**Beslutning E — Mal-bruk er en engangs-kopi, ingen vedvarende kobling.** +Når en org-`course` opprettes med en `personal_course` (eller en +TeeOff-bane) som utgangspunkt, kopieres dataene inn i den nye raden ved +opprettelsestidspunktet — ingen fremmednøkkel eller synk-mekanisme +knytter dem sammen etterpå. Samme filosofi som offisiell TeeOff-import +allerede etablerte (ADR-019): en mal er et utgangspunkt å redigere fritt +fra, ikke en levende referanse. Konsekvens: sletting av en +`personal_course`-mal påvirker ALDRI en org-`course`-rad som tidligere +ble opprettet fra den. + +**Konsekvens/bevisst utenfor omfang:** ingen moderasjon/vetting av +offentlige baner (tillitsbasert, matcher appens øvrige +lukkede-plattform-antagelser); ingen privat/kun-min-org-synlighet per +bane (alt publisert via denne veien er plattform-offentlig, ingen +per-rad-innstilling bygget). Se CHANGELOG.md 2026-08-04 for full +bygge-/verifiseringsdetalj (34/34 håndregnede API-sjekker, ekte +nettleser-verifisering på tvers av to brukere/to organisasjoner). +**Rullet ut live 2026-08-04**, bruker bekreftet eksplisitt. + +--- + ## Åpne spørsmål (ikke besluttet ennå) Disse må avklares før eller under de relevante fasene: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3973f7f..ee93ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7207,3 +7207,184 @@ Neste steg: teecup_api teecup_frontend`, begge containere boot-et rent, `/health`/`/dashboard` → 200. V0-zip-en slettet etter merge, per etablert rutine. + +19. **Baneoppsett i turneringer: rekkefølge, TeeOff-import og delt + bane-mal-bibliotek — BYGGET OG SCRATCH-VERIFISERT GRUNDIG 2026-08-04, + migrasjon 054, IKKE ENNÅ RULLET UT MOT ekte `teecup_db`.** Brukeren + oppdaget, mens hen brukte den live turneringsmodulen, at "Klasser" + (med sin Standardutslag-velger, ADR-041) vises FØR banen/runden i det + hele tatt er satt opp -- meningsløst å velge standardutslag for en + klasse før man vet hvilke utslag banen har. Bemerket samtidig at + TeeOff-henting av baner ikke så ut til å være tilgjengelig i + turneringsmodulen, og ba om en helt ny funksjon: la BÅDE + turneringsmodulen OG single-runde-modulen tilby "bruk en eksisterende + bane som mal" (fra TeeOff, eller fra en annens custom-bane) når man + oppretter en manuell bane. + + **Del I -- rekkefølge + TeeOff-import-gap (ingen skjemaendring):** + Explore-agent bekreftet klagen presist og avdekket at det var verre + for lagturneringer enn antatt: for individuelle turneringer lå + `ClassesCard` FØR `RoundsCard` i samme Oppsett-fane + (`individual-tournament-detail.tsx`); for lagturneringer lå Klasser + på selve FØRSTE fanen ("Lag og spillere"), mens bane-/rundeoppsett + krevde en hel sidenavigering til "Program". TeeOff offisiell-import + (ADR-019, ferdig bygget) var KUN koblet til lagturnerings-økt-UI-et, + fullstendig fraværende fra den individuelle turneringsflyten. + Fikset: `ClassesCard` flyttet til å rendres ETTER `RoundsCard` i + individuelle turneringer; Klasser-kortet flyttet fysisk fra + `tournament-detail.tsx` ("Lag og spillere") til `tournament-program.tsx` + ("Program"), rett etter økt-/rundeoppsettet -- `classes`-state (brukt + av `TeamPanel` til roster-klassevisning) ble værende i + `tournament-detail.tsx`, men create/delete-handlerne og selve + `ClassesCard`-komponentdefinisjonen flyttet med til `tournament-program.tsx`. + `OfficialCourseSearch`-komponentmønsteret (allerede i + `tournament-program.tsx`) kopiert inn i `individual-tournament-detail.tsx` + (samme duplisering-mellom-turneringstype-filer-konvensjon som + `ClassesCard` allerede fulgte), koblet til `NewRoundForm` sitt + eksisterende "+ Ny bane"-felt. + + **Det største funnet (Explore-agent, avdekket FØR bygging):** + turneringsmodulens "opprett manuell bane" var i praksis ubrukelig -- + KUN et navnefelt (`POST /orgs/{id}/courses` tok bare imot `{name}`), + ingen vei til hull/par/hcp-indeks/utslag i det hele tatt. To + sub-ressurs-endepunkter for å legge til dette i etterkant fantes + (`courses.py` sine `create_tee`/`create_holes`), men hadde ALDRI fått + noe frontend-kallsted -- funksjonen ble aldri fullført. + + **Del II -- mal-basert baneoppretting + delt offentlig bane-bibliotek.** + Et nøkkelfunn forenklet løsningen betraktelig: `personal_course` + (020_personal_rounds.sql, "frittstående runder") er, til tross for + navnet, ALLEREDE et globalt, plattform-omfattende bibliotek -- `GET + /personal-courses` søker på tvers av ALLE brukeres baner uten eier- + eller org-filtrering, og `POST /personal-courses` tar allerede imot + full hull-/utslagdata i ett atomisk kall. Ingen ny tabell trengtes -- + kun én kolonne (`forked_from_id`, migrasjon 054) for + proveniens/attribusjon. + Tre `AskUserQuestion`-runder avklarte omfanget presist FØR bygging + (samme disiplin som ADR-037/039/041): (1) ren reorder, ingen + blokkering; (2) et helt NYTT, kompakt hull-/utslag-editorskjema for + turneringsmodulen (speiler single-runde-modulens `OwnCreateStep` i + funksjon, egen stil), med "bruk som mal"-vei fra BÅDE TeeOff og + offentlig custom-bane; (3) alle tre moduler (individuell, + lagturnering, frittstående runde) skal ha funksjonen. + Et oppfølgingsspørsmål fra brukeren ("disse custom banene bør lagres, + og de må være offentlige... er det noe jeg ikke har tenkt på?") ble + tatt til en fjerde avklaringsrunde: (a) "offentlig" betyr HELE + TeeCup-plattformen (ikke bare egen org) -- eneste måte + single-runde-modulen faktisk kan dele samme mal-bibliotek som + turneringsmodulen, siden den ikke har noe org-begrep; (b) "Dupliser" + beholdes som egen, eksplisitt handling i tillegg til at redigering av + en ANNENS bane automatisk forker en kopi (redigering av DIN EGEN bane + endrer den i stedet, ingen fork). + + **Backend:** + - `054_personal_course_provenance.sql`: `personal_course.forked_from_id` + (selvreferende FK, `ON DELETE SET NULL`). Ingen RLS-endring -- + tabellen har aldri vært org-scopet (bevisst siden migrasjon 020). + - `rounds.py`: `PersonalCourseOut`/`PersonalCourseDetail` utvidet med + `is_mine`/`created_by_display_name` (attribusjon i mal-søket, FULLT + navn per navneformat-regelen -- dette er en administrasjonsvisning, + ikke direkte adressering) og (kun Detail) `holes`/`full_tees`/ + `forked_from_id` (forhåndsutfylling). Nye endepunkter: `PATCH + /personal-courses/{id}` (eier: oppdaterer i sted; IKKE eier: forker + automatisk -- ett endepunkt dekker begge casene uten at frontend må + forgrene på eierskap), `POST .../duplicate` (eksplisitt, uavhengig + av eierskap), `DELETE ...` (kun eier, 403 ellers; fanger opp + `asyncpg.ForeignKeyViolationError` fra `round.personal_course_id` + sin allerede-eksisterende `NO ACTION`-FK og gir en vennlig 409 + "IN_USE" -- IKKE via den delte `translate_db_errors()`, som ville + gitt feil retning/melding for akkurat denne casen). `GET + /personal-courses` fikk en `mine=true`-parameter (for + "Mine baner"-administrasjonen, unngår den vanlige 20-treffs- + begrensningen på navnesøket). + - `courses.py`: `POST /orgs/{id}/courses` utvidet til valgfritt å ta + imot samme `holes`/`tees`-form som `PersonalCourseCreate` -- + populert setter den BÅDE org-`course`-raden (med hull/utslag, + samme SQL-mønster som de eksisterende men ubrukte sub-ressurs- + endepunktene) OG en tilsvarende `personal_course`-rad (eid av + innlogget bruker, "offentliggjøringen" brukeren ba om) i SAMME + transaksjon -- ingen vedvarende kobling mellom de to radene etterpå + (samme "engangs-kopi"-filosofi som offisiell TeeOff-import, + ADR-019). `official-import`-endepunktet selv er UENDRET. + - `rounds.py` sin `GET /rounds/official-search/{slug}` (personlig + rundemodul) utvidet med fulle `holes`/`full_tees` PER TeeOff-bane + (kun populert når teeoff-dataene er komplette nok -- samme + fullstendighetskrav som ADR-019-importen allerede håndhever) -- + nødvendig fordi frittstående runder ALDRI persisterer teeoff-baner + (ADR-033 Beslutning C, live oppslag), så "bruk som mal" der må få + dataene tilbake i selve søkesvaret i stedet for å runde-trippe + gjennom en lagret rad slik org-siden kan. + + **Frontend:** + - Ny delt fil `course-template-editor.tsx`: `CourseTemplateEditor` + (kompakt hull-/utslag-skjema, forhåndsutfyllbar via `initial`-prop, + ren UI + lokal validering -- kalleren avgjør hvor data persisteres, + matcher prosjektets etablerte adaptermønster) + `CourseTemplatePicker` + (velger: "Fra bunnen av" / "TeeOff-bane som mal" / "Offentlig bane + som mal" -- sistnevnte med BÅDE "Bruk direkte" og "Tilpass før + bruk"). Brukt fra `tournament-program.tsx` (både ny økt OG endre + eksisterende økts bane) og `individual-tournament-detail.tsx` + (`NewRoundForm`). + - `new-round.tsx`: `OwnCreateStep` utvidet med valgfrie + `initial`/`forkedFromId`-props (samme prefyll-mekanisme). Nye + "Bruk som mal for egen bane"-knapper i BÅDE `CourseList` (TeeOff- + baner, kun synlig når komplette maldata finnes) og `OwnSearch` + (offentlige custom-baner, nå med attribusjon "Opprettet av X" / + "Opprettet av deg" -- samme navneformat-regel som backend). + - `account-settings.tsx`: ny "Mine baner"-seksjon (`MyCoursesSection`) + -- liste over EGNE `personal_course`-rader (`?mine=true`), med + Rediger (gjenbruker `CourseTemplateEditor`, PATCH), Dupliser + (POST duplicate) og Slett (DELETE, med vennlig 409/i-bruk-melding) + per rad. Andres baner vises IKKE her -- kun i mal-søket i de + respektive modulene. + + **Reell bug funnet og rettet UNDER scratch-verifisering** (ikke + antatt riktig fra koden alene): `CourseTemplateEditor` ble først + bygget med sitt eget `