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

10 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-bug (funnet 2026-07-16, IKKE fikset ennå)

  • Status: trenger egen rettingsrunde (lavrisiko, men berører ADR-003s isolasjonsgrunnmur — fortjener fokusert testing, ikke en hastefiks).
  • Alle RLS-policyer i 001/003 (org_isolation på 13 tabeller + org_selforganization) bruker current_setting('app.current_org', true)::uuid. Denne håndterer NULL trygt (gir ingen rader, som tiltenkt), men IKKE tomstreng — og en gjenbrukt asyncpg-pool-tilkobling der en TIDLIGERE forespørsel satte GUC-en via SET LOCAL kan lese den tilbake som '' (tomstreng) i stedet for NULL etter at den transaksjonen er ferdig. Da kaster casten en 500 (invalid input syntax for type uuid: "") i stedet for skjemaets lovede "trygg standard: se ingenting".
  • Oppdaget av: /auth/me (ny denne runden) prøvde å joine mot organization-tabellen via plain_connection() (ingen org-kontekst) for å hente organisasjonsnavn til en multi-org-liste — det er FØRSTE gang noe spør en org-scopet, RLS-beskyttet tabell via en tilkobling uten org-kontekst. Mitigert MIDLERTIDIG i /auth/me ved rett og slett å ikke joine mot organization (returnerer kun organization_id + role, ikke navn) — unngår buggen, løser den ikke.
  • Fiks: NULLIF(current_setting('app.current_org', true), '')::uuid i stedet for current_setting(...)::uuid, i alle 15 policyer (ALTER POLICY, egen migrasjon 005). NULLIF konverterer tomstreng til NULL FØR cast, så den trygge "se ingenting"-oppførselen gjenopprettes uansett hvilken tilstand GUC-en er i.
  • Følgeoppgave når fikset: /auth/me kan da trygt joine mot organization igjen og returnere organisasjonsnavn, ikke bare ID+rolle.

Ø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).