teecup/FEATURE_BACKLOG.md
Erol Haagenrud 9a42a2e585 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

11 KiB

TeeCup — Funksjons-backlog

Formål: fange ALT som er ønsket for TeeCup på ett sted, slik at intet krav går tapt mellom økter, modeller eller samtaler. Kilder: de to opprinnelige Gemini-samtalene (funksjonalitet + Docker) og arbeidet gjort med Claude.

Status-koder: ferdig · 🔨 pågår · 📋 planlagt/fanget · trenger beslutning · 🔀 endret fra opprinnelig råd · 💤 utsatt (bevisst)

Sist oppdatert: 2026-07-16


Fundament (bygget)

Funksjon Status Notat
Handicap-/matchmotor (testet) 24 tester, R&A-verifisert. Erstatter Geminis løse JS-funksjon.
Formater: singles, fourball, foursome, greensome, scramble I motoren, allowance som konfig.
Brutto/netto + prosent-allowance (75 %, 90 %, 3/4) ADR-005. Prosentene bekreftet mot Golf GameBook (ADR-014).
9-hulls (front/back) slagfordeling ADR-008, egen funksjon + test.
Miksede formater per turnering (økter) ADR-007. Ryder Cup-strukturen.
Spiller-pool m/ reserver, delvis deltakelse ADR-007.
Multi-tenant skjema + RLS ADR-001/003. Isolasjon bevist med oppførselstest.
Dedikert app-rolle (ingen superuser/BYPASSRLS) migrasjon 002.
API: oppsett (spillere/lag/roster/økter/matcher/blind draw) Verifisert for ekte mot scratch-db + engangscontainer.
API: scoring (hole_score/match_hole_result, matchstatus, handicap-beregning) ADR-012/014. Verifisert for ekte, inkl. fourball better-ball og race-sikker recompute.
Banedata fra teeoff via API 🔀 ADR-004. Endret fra Geminis «delt database direkte».
Konfigurerbar handicap-pipeline (4 brytere) ADR-014. Bygget i app/handicap.py, brukt av scoring-runden.
Ekte autentisering (magic-link + JWT-sesjon) ADR-009. app/routers/auth.py + migrasjon 004_auth.sql. X-Debug-User-Id-stubben er helt fjernet. Ekte SMTP-utsending gjenstår (i dag: dev-only logging).
RLS-tomstreng-fiks (app_current_org()) Migrasjon 005_rls_null_guard.sql. Se detaljer under.

RLS-tomstreng-bug — FIKSET 2026-07-16

  • Alle RLS-policyer i 001/003 (org_isolation på 14 tabeller + org_selforganization) brukte current_setting('app.current_org', true)::uuid. Denne håndterte NULL trygt (ga ingen rader, som tiltenkt), men IKKE tomstreng — en gjenbrukt asyncpg-pool-tilkobling der en TIDLIGERE forespørsel satte GUC-en via SET LOCAL kunne lese den tilbake som '' etter at transaksjonen var ferdig, og casten kastet da en 500 i stedet for skjemaets lovede "trygg standard: se ingenting".
  • Fiks: samlet i én STABLE SQL-funksjon app_current_org() (migrasjon 005_rls_null_guard.sql) som gjør NULLIF(current_setting(...), '')::uuid — konverterer tomstreng til NULL FØR cast. Alle 15 policyer alteret (ALTER POLICY) til å bruke funksjonen i stedet for det rå uttrykket. Verifisert med 3 nye regresjonstester i test_isolation.sql (Test 10-12: tomstreng-lesing gir 0 rader ikke krasj, tomstreng-skriving avvises av RLS ikke krasj, org-bootstrap-innsetting fungerer rett etter tomstreng- tilstand) OG ved å faktisk gjenskape original-buggen mot en ekte container (pool-størrelse 1, varm opp med org_connection(), deretter kall /auth/me på samme gjenbrukte tilkobling — gikk fra 500 til 200).
  • Viktig presisering oppdaget underveis: den opprinnelige planen antok at /auth/me kunne joine organization direkte igjen når tomstreng-buggen var fikset. Det var FEIL — fiksen gjør bare at tomstreng oppfører seg som NULL (trygt: se ingenting), den endrer IKKE at org_self-policyen krever en MATCHENDE app.current_org for å vise en rad i det hele tatt (riktig RLS-oppførsel, ikke en bug). 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 spørringer, N = antall org-er brukeren tilhører, typisk 1-3) — verifisert at dette faktisk returnerer navnet korrekt.

Ønsket, men IKKE fanget før nå (fra Gemini-samtalene)

Brukerroller (utover org-medlemskap)

  • Status: trenger beslutning
  • De opprinnelige samtalene beskriver: turneringsadmin, lagkaptein, spiller, tilskuer (les-only, følger live uten skriverettigheter).
  • Vi har i dag org-roller (owner/admin/member) + is_captain på roster.
  • Mangler: «tilskuer» som begrep. Henger sammen med hvem som ser den offentlige feeden (se Kommunikasjon). Kaptein-rollen bør kanskje gi spesifikke rettigheter (sette oppstilling), ikke bare være et flagg.

Scoring-autorisasjon: hvem fører, hvem korrigerer, hvem lukker (rejst 2026-07-16)

  • Status: trenger beslutning — direkte oppfølger av «Brukerroller» over.
  • Hvem fører score i dag: alle med en team_roster-rad på laget (samme minimale grense som deltaker/lås, se ADR-013-relatert kode). Ikke kaptein-only, ikke begrenset til de(n) som faktisk spiller matchen.
  • Hvem kan korrigere en ført score i dag: akkurat de samme — skriving er en upsert (ON CONFLICT ... DO UPDATE), så en korrigering er ikke skilt fra en førstegangs-innføring. Ingen godkjenning fra motstanderen, ingen audit-trail (forrige verdi overskrives sporløst).
  • Match-lås ved avgjørelse: FIKSET 2026-07-16. submit_hole_score og submit_hole_result (app/routers/scoring.py) avviser nå med 409 («Matchen er avgjort og kan ikke lenger endres.») FØR upserten kjøres, hvis match.points_side_a IS NOT NULL — dette er allerede et pålitelig, entydig signal siden kolonnen kun settes når compute_match_state sier matchen er avgjort. Gjelder BÅDE nye hull og korrigering av allerede talte hull. Verifisert: hull etter avgjørelse avvist, korrigering av et talt hull avvist, GET scorecard fortsatt leselig, ikke-avgjorte matcher upåvirket. Bevisst IKKE bygget: ingen manuell «avslutt match før den er avgjort»-handling (det grenser mot walkover under, fortsatt utsatt). Turnering har et status-felt (draft/active/completed/archived) men INGEN endepunkt endrer det ennå — organisator kan ikke markere en turnering ferdig via API-et i dag (eget, senere punkt).
  • Manglende WO/konsesjon: hvis en side aldri stiller nok spillere (match_participant-antallet når aldri det økten krever), beregnes handicap ALDRI (se app/handicap.py), og matchen kan derfor ALDRI få et hull-resultat — den blir hengende uavgjort for alltid. Det finnes ingen «gi bort hullet/matchen/turneringen»-mekanisme (walkover/konsesjon) i det hele tatt ennå — verken datamodell eller endepunkt.
  • Avgjort 2026-07-16:
    • Match-lås ved avgjørelse: bygget — se eget punkt over. Automatisk (ikke en handling noen utfører), så «hvem får låse» ble aldri et spørsmål som trengte avklaring.
    • Walkover/konsesjon VENTER til brukerroller (kaptein/organisator, punktet over) er avgjort — å bygge den nå på dagens løse «rostret på laget»-grense betyr sannsynligvis å bygge den om senere.
  • Fortsatt åpent: (a) skal score-føring begrenses til faktiske matchdeltakere (ikke bare «noen på laget»)? (b) skal korrigering kreve motpartens godkjenning, eller er upsert-modellen god nok for v1? (c) skal turnering-status (draft/active/completed/archived) kunne settes via API?

Blind draw (skjult lagoppstilling)

  • Status: skjema (migrasjon 003, lineup_lock) + API bygget og verifisert (app/routers/matches.py: synlighetsfilter i SQL, ikke Python-filter — se ADR-013).
  • Kapteinene låser oppstillingen skjult; matchene avsløres samtidig når begge er ferdige.
  • Gjenstår: hvem som FÅR låse et lag er i dag bare «rostret på laget», ikke kaptein-spesifikt — se «Brukerroller» over.

Forenklet scoreføring (uten slagtall)

  • Status: skjema (migrasjon 003) + API bygget og verifisert (app/routers/scoring.py: hole-scores for stroke-modus, hole-results for hole_result-modus, begge mater samme compute_match_state).

Bøtekasse (Kangaroo Court)

  • Status: 📋 planlagt
  • Meld inn overtramp med bøter («kastet kølla på hull 4»).
  • Henger sammen med Kommunikasjon: bøter deles i feeden. Sannsynligvis en egen post-type i meldingsmodellen.

Flerårig statistikk / historikk / MVP

  • Status: 📋 planlagt (datamodell støtter det allerede)
  • Seiersprosent i singelmatcher, historisk MVP, statistikk over år.
  • Datamodellen tillater dette fordi spillere lever på org-nivå og gjenbrukes på tvers av turneringer, og handicap fryses per turnering (reproduserbart).

Push-varsler

  • Status: 📋 planlagt (infrastruktur)
  • «BREAKING: X vant matchen». PWA push. Egen infrastruktur-bit.

Sanntid (WebSockets)

  • Status: trenger beslutning
  • Live leaderboard og chat som oppdateres uten refresh. Vi har leaderboard-viewet, men ikke sanntidsleveringen. Valg: WebSockets vs. polling.

Knockout / cup-turnering (egen turneringstype)

  • Status: 💤 utsatt (egen fremtidig type, ADR-011)
  • Utslagsmatcher: 128 spillere → finale + bronsefinale. Rundene er avhengige (vinner går videre), krever bracket/progresjon, seeding, fripass. IKKE i v1.
  • v1 er låst til nøyaktig to lag (Ryder Cup-format). Match-modellen er generell, så denne typen kan legges til senere uten dataomskriving — den legger bare til et progresjons-lag.

Kommunikasjon (under design)

Del Status Notat
Lag-intern chat («det hemmelige rommet») 🔨 Bekreftet ønsket. Kanal m/ scope team.
Offentlig runde-feed («Banter Board») Synlighetsnivå ikke besluttet (deltakere/org/offentlig lenke).
Bilder i feed/chat 📋 v1. Objektlagring (MinIO), presigned opplasting.
Video 💤 Arkitekt for det, bygg senere (ADR-forslag).
1-til-1 direktemeldinger 💤 Gemini frarådet for v1; ikke etterspurt av deg.
Moderering (for offentlig innhold) Kreves hvis «alle med lenken». Mønster finnes i teeoff.

UX / frontend (senere fase)

  • 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp (banebruk i sollys/med solbriller).
  • Offline-first (ADR-006) — prinsipp besluttet; implementasjon senere.
  • 📋 PWA: manifest, service workers, «Legg til på hjemskjerm».

Bevisst endret fra opprinnelige (Gemini-)råd

  • 🔀 Banedata: API mot teeoff (ADR-004), IKKE direkte delt database. Direkte DB-kobling ville låst TeeCup til teeoffs skjemaendringer.
  • 🔀 Handicap-motor: egen testet Python-modul, ikke den innlimte JS-funksjonen (som bl.a. ikke håndterte 9-hull eller konfigurerbare allowances korrekt).
  • 🔀 Tenant-modell: organisasjon som tenant med RLS, ikke bare «turnering-ID».
  • 🔀 Sesjons-secret: egne secrets for TeeCup, ikke fallback til teeoffs (teeoff selv bruker en slik fallback — bevisst unngått her).