diff --git a/020_personal_rounds.sql b/020_personal_rounds.sql new file mode 100644 index 0000000..c2f01a2 --- /dev/null +++ b/020_personal_rounds.sql @@ -0,0 +1,209 @@ +-- ===================================================================== +-- TeeCup — frittstående rundeføring med detaljert statistikk +-- (migrasjon 020, ADR-033) +-- ===================================================================== +-- Frittstående runder er eid av en BRUKER (`app_user.id`), ikke en +-- organisasjon (Beslutning A) -- INGEN RLS på disse tabellene. Samme +-- allerede etablerte mønster som personlig profil (015)/HCP-historikk +-- (018)/sekundær e-post (017): `plain_connection()` (ingen +-- `app.current_org`), autorisasjon håndheves eksplisitt i app-laget med +-- `WHERE owner_user_id = $1` i hver spørring. +-- +-- Banedata (Beslutning C, bekreftet med bruker): offisielle teeoff-baner +-- slås opp LIVE ved behov (ingen lokal kopi/import, kun en referanse + +-- et navne-snapshot for visning). Egendefinerte baner som ikke finnes i +-- teeoff havner i en NY, GLOBAL banekatalog (`personal_course` + +-- tilhørende hull/utslag/rating-tabeller) -- delt på tvers av ALLE +-- TeeCup-brukere (ikke org-scopet som den eksisterende `course`-tabellen), +-- med søk-før-opprett tenkt håndtert i API-/frontend-laget for å begrense +-- duplikater (ikke håndhevet i skjemaet). +-- +-- `personal_course_tee_rating` har BEVISST ingen `scope`-kolonne (ulikt +-- den org-scopede `tee_rating`) -- Beslutning G sin Net-Par-tilnærming +-- for uspilte hull gjør at frittstående runder alltid regnes som en +-- 18-hulls-ekvivalent gjennom ÉN formel, uansett om 9 eller 18 hull +-- faktisk ble spilt. Ingen egen front_9/back_9-rating trengs derfor her. +-- +-- Hver runde snapshotter par/stroke-index PER SPILT HULL på selve +-- `round_hole`-raden (samme reproduserbarhets-prinsipp som ADR-007 sin +-- handicap-snapshot) -- en fremtidig endring i teeoff sin banedata, eller +-- en redigering av en `personal_course`, skal ALDRI endre en allerede +-- spilt rundes tall retroaktivt. +-- +-- Statistikk-modellen (Beslutning B): et fast sett navngitte felt per +-- hull, ikke fri slag-for-slag-logging. GIR er bevisst IKKE en egen +-- lagret kolonne -- den er alltid DERIVERBAR ved lesing +-- (`approach_result = 'hit' AND (score - putts) <= par - 2`), se +-- ADR-033. +-- +-- Bevisst v1-avgrensning: en `round` er kun synlig/redigerbar for sin +-- `owner_user_id` -- en lenket deltaker (`round_participant.user_id` +-- satt) får IKKE egen tilgang til runden i denne runden (notert som +-- åpent punkt i ADR-033, ikke løst her). +-- ===================================================================== + +\set ON_ERROR_STOP on + +-- --------------------------------------------------------------------- +-- Global banekatalog for egendefinerte (ikke-teeoff) baner +-- --------------------------------------------------------------------- + +CREATE TABLE personal_course ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name text NOT NULL, + created_by_user_id uuid NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE personal_course_hole ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + personal_course_id uuid NOT NULL REFERENCES personal_course(id) ON DELETE CASCADE, + hole_number smallint NOT NULL CHECK (hole_number BETWEEN 1 AND 18), + par smallint NOT NULL CHECK (par BETWEEN 3 AND 6), + stroke_index smallint NOT NULL CHECK (stroke_index BETWEEN 1 AND 18), + UNIQUE (personal_course_id, hole_number), + UNIQUE (personal_course_id, stroke_index) +); + +CREATE TABLE personal_course_tee ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + personal_course_id uuid NOT NULL REFERENCES personal_course(id) ON DELETE CASCADE, + name text NOT NULL, + UNIQUE (personal_course_id, name) +); + +CREATE TABLE personal_course_tee_rating ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + personal_course_tee_id uuid NOT NULL REFERENCES personal_course_tee(id) ON DELETE CASCADE, + -- Kun m/f, ikke 'x' -- en WHS-rating er alltid for ett bestemt kjønn + -- (samme presedens som org-scopet tee_rating, ADR-029). + gender text NOT NULL CHECK (gender IN ('m', 'f')), + course_rating numeric(4,1) NOT NULL, + slope_rating smallint NOT NULL CHECK (slope_rating BETWEEN 55 AND 155), + par smallint NOT NULL, + UNIQUE (personal_course_tee_id, gender) +); + +-- --------------------------------------------------------------------- +-- Selve runden +-- --------------------------------------------------------------------- + +CREATE TABLE round ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + owner_user_id uuid NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + + course_source text NOT NULL CHECK (course_source IN ('teeoff', 'custom')), + -- 'teeoff': live oppslag, kun referanse + navn-snapshot lagres. + teeoff_facility_slug text, + teeoff_course_id text, + -- 'custom': ekte FK til den globale katalogen over. + personal_course_id uuid REFERENCES personal_course(id), + CHECK ( + (course_source = 'teeoff' + AND teeoff_facility_slug IS NOT NULL AND teeoff_course_id IS NOT NULL + AND personal_course_id IS NULL) + OR + (course_source = 'custom' + AND personal_course_id IS NOT NULL + AND teeoff_facility_slug IS NULL AND teeoff_course_id IS NULL) + ), + -- Snapshot for visning uten et nytt live-oppslag hver gang runden åpnes. + course_name_snapshot text NOT NULL, + tee_name_snapshot text NOT NULL, + + played_at date NOT NULL, + -- Fritt starthull (Beslutning E) -- 9/18 under er kun et UI-standardvalg, + -- IKKE håndhevet (spilleren kan avslutte etter et vilkårlig antall hull). + start_hole smallint NOT NULL DEFAULT 1 CHECK (start_hole BETWEEN 1 AND 18), + holes_planned smallint NOT NULL DEFAULT 18 CHECK (holes_planned IN (9, 18)), + + completed_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX ON round (owner_user_id); + +-- --------------------------------------------------------------------- +-- Deltakere i runden -- eieren selv ELLER andre i flighten (Beslutning D) +-- --------------------------------------------------------------------- + +CREATE TABLE round_participant ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + round_id uuid NOT NULL REFERENCES round(id) ON DELETE CASCADE, + + -- Nøyaktig én av user_id/guest_name -- en ekte TeeCup-bruker som + -- spiller med, ELLER et rent navn uten konto (samme prinsipp som + -- org-scopet `player` uten `user_id`). + user_id uuid REFERENCES app_user(id), + guest_name text, + CHECK ( + ((user_id IS NOT NULL)::int + (guest_name IS NOT NULL)::int) = 1 + ), + + is_owner boolean NOT NULL DEFAULT false, + + -- Snapshot ved tilføyelse (samme reproduserbarhets-prinsipp som ADR-007 + -- sin handicap-snapshot) -- en senere profilendring skal ikke endre en + -- allerede spilt rundes tall retroaktivt. + gender text NOT NULL CHECK (gender IN ('m', 'f', 'x')), + handicap_index_snapshot numeric(4,1), -- NULL = ingen HCP-sporing for denne deltakeren + course_handicap_snapshot smallint, + + -- Fylles ved fullføring (ADR-033 Beslutning F/G) -- kun sant/satt når + -- minimums-/gyldighetskravene er oppfylt. + counts_for_handicap boolean NOT NULL DEFAULT false, + score_differential numeric(4,1), + + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX ON round_participant (round_id); +CREATE INDEX ON round_participant (user_id); +-- Maks én markert eier per runde. +CREATE UNIQUE INDEX round_participant_one_owner ON round_participant (round_id) WHERE is_owner; + +-- --------------------------------------------------------------------- +-- Hull-for-hull -- rating-snapshot OG statistikk, per deltaker +-- --------------------------------------------------------------------- + +CREATE TABLE round_hole ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + round_participant_id uuid NOT NULL REFERENCES round_participant(id) ON DELETE CASCADE, + hole_number smallint NOT NULL CHECK (hole_number BETWEEN 1 AND 18), + + -- Rating-snapshot (par/stroke-index på TIDSPUNKTET runden ble spilt) -- + -- se moduldoc-kommentaren øverst om hvorfor dette IKKE er en live-referanse. + par smallint NOT NULL CHECK (par BETWEEN 3 AND 6), + stroke_index smallint NOT NULL CHECK (stroke_index BETWEEN 1 AND 18), + + played boolean NOT NULL DEFAULT false, + score smallint CHECK (score IS NULL OR score BETWEEN 1 AND 20), + putts smallint CHECK (putts IS NULL OR putts BETWEEN 0 AND 10), + club_off_tee text, + -- Kun meningsfullt på par 4/5 (håndheves i app-laget, ikke her). + tee_shot_result text CHECK (tee_shot_result IS NULL OR tee_shot_result IN ('fairway', 'left', 'right')), + -- "Innspillsslaget" = siste slag før første putt, uansett hullets par + -- (ADR-033 Beslutning B) -- GIR er DERIVERT herfra ved lesing, ikke en + -- egen lagret kolonne: approach_result='hit' AND (score-putts) <= par-2. + approach_result text CHECK (approach_result IS NULL OR approach_result IN ('hit', 'long', 'short', 'left', 'right')), + chip_count smallint CHECK (chip_count IS NULL OR chip_count >= 0), + bunker_shot_count smallint CHECK (bunker_shot_count IS NULL OR bunker_shot_count >= 0), + penalty_strokes smallint CHECK (penalty_strokes IS NULL OR penalty_strokes >= 0), + first_putt_distance_m numeric(4,1) CHECK (first_putt_distance_m IS NULL OR first_putt_distance_m >= 0), + + UNIQUE (round_participant_id, hole_number) +); + +-- --------------------------------------------------------------------- +-- Rettigheter -- INGEN RLS her (Beslutning A). teecup_app filtrerer selv +-- med WHERE owner_user_id = $1 (round) / gjennom round_id (de to andre) i +-- hver spørring, samme mønster som handicap_history/user_secondary_email. +-- --------------------------------------------------------------------- + +GRANT SELECT, INSERT, UPDATE, DELETE ON personal_course TO teecup_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON personal_course_hole TO teecup_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON personal_course_tee TO teecup_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON personal_course_tee_rating TO teecup_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON round TO teecup_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON round_participant TO teecup_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON round_hole TO teecup_app; diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index 9ea78aa..26e75d5 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -1829,6 +1829,30 @@ finnes? Påvirker om `handicap_history` (018) skal gjenbrukes uendret eller trenger en ny kolonne som skiller "manuelt satt" fra "beregnet fra runde". +**Skjemaet (migrasjon `020_personal_rounds.sql`) er ✅ SKREVET OG +SCRATCH-VERIFISERT 2026-07-22,** som andre byggesteg (etter motoren). +Sju nye tabeller: `round` (header, `owner_user_id`-eid, ingen RLS), +`round_participant` (deltakere — lenket bruker ELLER gjestenavn, XOR- +håndhevet via CHECK; maks én markert eier per runde via partiell unik +indeks), `round_hole` (rating-SNAPSHOT + statistikk per deltaker per +hull — GIR er bevisst IKKE en egen kolonne, kun deriverbar ved lesing: +`approach_result='hit' AND (score-putts) <= par-2`, verifisert eksakt +mot ekte testdata), pluss fire tabeller for den globale banekatalogen +(`personal_course`/`_hole`/`_tee`/`_tee_rating` — sistnevnte BEVISST uten +`scope`-kolonne, ulikt org-tabellen, siden Net-Par-tilnærmingen gjør +9-hulls-spesifikk rating overflødig). `round`/`personal_course_*` har +INGEN RLS (Beslutning A) — autorisasjon i app-laget. +**Scratch-verifisert, 9 sjekker** (kjørt som `teecup_app_scratch`, ikke +superbruker): duplikat kjønn på samme utslag avvist, teeoff+custom-felt +samtidig avvist (CHECK), verken/begge user_id+guest_name avvist (XOR- +CHECK), to markerte eiere på samme runde avvist, duplikat hullnummer per +deltaker avvist, ugyldig approach_result-verdi avvist, kaskade-sletting +av en runde fjerner alle dens deltakere+hull men lar ANDRE runder stå +urørt. `test_isolation.sql` fortsatt 12/12 (ingen RLS-regresjon på +eksisterende tabeller). **Gjenstår:** API-lag og frontend — ingen av +disse er startet, og migrasjonen er IKKE kjørt mot ekte `teecup_db` ennå +(venter på egen bekreftelse, som all annen skriving mot ekte database). + **Motor-komponenten (punkt 1-11) er ✅ BYGGET OG TESTET 2026-07-22,** som første, isolerte byggesteg (ren Python, ingen DB/API/frontend ennå — matcher ADR-005s "test i isolasjon FØR resten"). Nye funksjoner i @@ -1884,12 +1908,14 @@ Alle tre store åpne punktene fra første utkast (banedata, WHS 9-hulls- regel, full HCP-indeksformel) er enten eksplisitt bekreftet med bruker (Beslutning C) eller presist kildebelagt fra den offisielle WHS Rules of Handicapping 2024 (Beslutning F/G) — ikke lenger antatt eller tilnærmet. -**Første byggesteg ferdig samme dag:** hele HCP-indeks-motor-komponenten -(Beslutning G, punkt 1-11) er bygget og testet i `handicap_engine.py` -(41/41 tester, se detalj under Beslutning G). Gjenstår: migrasjon (nye -tabeller for runde/deltaker/statistikk), API-lag, frontend — samme -inkrementelle, scratch-verifiserte rytme som resten av prosjektet, ingen -av disse er startet ennå. +**To byggesteg ferdig samme dag:** (1) hele HCP-indeks-motor-komponenten +(Beslutning G, punkt 1-11) bygget og testet i `handicap_engine.py` +(41/41 tester), (2) full databasemigrasjon (`020_personal_rounds.sql`, +sju tabeller) skrevet og scratch-verifisert (9 sjekker, se detalj under +Beslutning G/skjema-avsnittet). Gjenstår: API-lag, frontend, OG selve +kjøringen av migrasjonen mot ekte `teecup_db` (venter på egen +bekreftelse) — samme inkrementelle, scratch-verifiserte rytme som resten +av prosjektet. --- diff --git a/CLAUDE.md b/CLAUDE.md index 0664543..8ae353a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,6 +46,22 @@ Les dette først i hver økt. Det koder hva vi har bestemt og hvordan vi jobber. - Media (bilder/video) skal i objektlagring (MinIO), ikke i Postgres. Postgres holder bare metadata + nøkkel. +## Tilgjengelighet — frontend (ufravikelig, gjelder ALT, eksisterende og fremtidig) +- Brukeren instruerte eksplisitt 2026-07-22: ALL frontend — det som + allerede finnes OG alt som designes med V0 fremover — skal være + lesbart, forståelig og betjenbart for noen med noe redusert syn UTEN + briller, så langt det praktisk lar seg gjøre. Dette er en STÅENDE + forventning til alt fremtidig UI-arbeid, ikke en engangsting for én + skjerm. +- Praktisk konsekvens: god kontrast, stor nok skrift, store nok + trykkflater, ikke ikon-only uten tekst-label for viktige handlinger, + ikke avhengig av finmotorikk/skarpt syn for å bruke appen. +- Gjelder begge retninger: (a) ta dette eksplisitt med som krav når en ny + V0-prompt skrives eller en V0-eksport gjennomgås, og (b) rett + opportunistisk opp eksisterende skjermer når de likevel røres i en + annen runde — ingen egen stor retrofit-runde er igangsatt eller bedt + om ennå. + ## Arbeidsmåte - Inkrementelt. Ingenting tas for gitt før det er testet. Bekreft hvert steg før du går videre. @@ -2306,9 +2322,17 @@ Neste steg: uten pytest da det ikke er installert i miljøet), flere verifisert mot regelbokens egne tallregneeksempler (Rule 5.2a, Rule 5.1c, Diagram 5.8, Diagram 3.1b). Ren Python, ingen DB/API/frontend rørt ennå — matcher - ADR-005s "test i isolasjon FØR resten". Se ADR-033 i - ARCHITECTURE_DECISIONS.md for full detalj og gjenstående steg - (migrasjon, API, frontend — ingen påbegynt). + ADR-005s "test i isolasjon FØR resten". + **Deretter, samme dag:** full databasemigrasjon skrevet + (`020_personal_rounds.sql`, sju nye tabeller: `round`/ + `round_participant`/`round_hole` + fire for en global egendefinert- + bane-katalog) og scratch-verifisert (9 sjekker som + `teecup_app_scratch`, ikke superbruker — CHECK-constraints, XOR + user_id/guest_name, kaskade-sletting, GIR-derivering bekreftet mot + ekte data). INGEN RLS på disse tabellene (Beslutning A). `test_ + isolation.sql` fortsatt 12/12. **IKKE kjørt mot ekte `teecup_db` + ennå** — venter på egen bekreftelse. API-lag og frontend heller ikke + startet. Se ADR-033 i ARCHITECTURE_DECISIONS.md for full detalj. 4. **Del 1 (fri, ukrevd sekundær-e-post) er nå BYGGET OG LIVE** (2026-07-21, se status over). **Del 2 (ekte konto-sammenslåing) fortsatt IKKE designet:** hva skjer hvis den ønskede adressen ALLEREDE tilhører en diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index 913a0ec..a96dd4f 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -1368,6 +1368,12 @@ tom-skjermens endelige form kan bestemmes. ## Frittstående rundeføring + detaljert statistikk (uten turnering/organisasjon) — 🔨 ADR-033 SKREVET OG KILDEBELAGT, BYGGING PÅBEGYNT 2026-07-22 +**Fremdrift 2026-07-22:** HCP-indeks-motor bygget+testet +(`handicap_engine.py`, 41/41 tester) OG full databasemigrasjon skrevet+ +scratch-verifisert (`020_personal_rounds.sql`, 9 sjekker). Se ADR-033 i +ARCHITECTURE_DECISIONS.md for full detalj. Gjenstår: kjøre migrasjonen mot +ekte `teecup_db` (venter på bekreftelse), API-lag, frontend. + **Se ADR-033 i ARCHITECTURE_DECISIONS.md for den fulle, besluttede arkitekturen** (eierskapsmønster, statistikk-datamodell, HCP-indeksmotor). Brukeren bekreftet 2026-07-22 at teeoff-banedata skal hentes via LIVE @@ -1640,8 +1646,17 @@ dedikert designrunde. ## UX / frontend (senere fase) +- 🔀 **Tilgjengelighet — STÅENDE krav, ikke lenger et enkeltpunkt + (skjerpet 2026-07-22, se CLAUDE.md):** all frontend, eksisterende og + fremtidig (inkl. alle nye V0-skjermer), skal være lesbar/forståelig/ + betjenbar for noen med noe redusert syn UTEN briller. Det tidligere, + vagere punktet under ("høy kontrast, store knapper...") er nå en + KONKRET instans av dette generelle, varige kravet — ikke en egen, + isolert senere-fase-oppgave. Ingen dedikert retrofit-runde igangsatt + ennå; rettes opportunistisk når skjermer likevel røres, og tas inn i + enhver ny V0-prompt fremover. - 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp - (banebruk i sollys/med solbriller). + (banebruk i sollys/med solbriller) — konkret eksempel på punktet over. - ✅ Offline-first (ADR-006) — BYGGET 2026-07-19 (ADR-028), se eget punkt under. Scoreregistrering (hole-scores/hole-results) fungerer nå offline med automatisk synk.