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

18 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.
RLS-tomstreng-fiks (app_current_org()) Migrasjon 005_rls_null_guard.sql. Se detaljer under.
Organisasjon-bootstrap (opprette ny org via API) POST /orgs, app/routers/organizations.py. Se detaljer under.
Ekte SMTP-utsending av magic-link app/email.py. Se detaljer under.
Containerisert, LIVE på teecup.teeoff.no Dockerfile + docker-compose.yml. Se egen seksjon under — to reelle driftshendelser funnet og rettet.

Containerisering og go-live — FERDIG 2026-07-16

  • POST /orgs osv. var siste kodebit; dette var første gang noe rørte EKTE, PERMANENT infrastruktur (ekte teecup_db, ekte langtlevende container, den DELTE Caddy-instansen som også ruter live teeoff.no).
  • Bygget: ekte teecup_db opprettet, migrasjoner 001→005 kjørt permanent (samme filer, ingen endringer), test_isolation.sql bestått (ruller alltid tilbake, trygt å kjøre mot en database som skal bli stående). Dockerfile (speiler scratch-rundenes bevist-riktige volumoppsett: app/ + handicap_engine.py på samme relative plassering) + docker-compose.yml (tjeneste teecup_api, joiner det eksisterende, eksterne teeoff_default-nettverket). Ny Caddy-blokk for teecup.teeoff.no i /opt/teeoff/deploy/Caddyfile.
  • To reelle driftshendelser, begge funnet og rettet i sanntid:
    1. Stale bind-mount-inode: teeoff_caddy sin Caddyfile-mount er en ENKELTFIL-bind-mount, låst til inoden som fantes da containeren sist startet (13 dager tidligere). Fil-redigering via atomisk rename laget en ny inode på samme sti — containeren fortsatte å lese den GAMLE filen uansett hvor mange ganger caddy validate/reload/admin-API /load ble kjørt (alle opererte på den uendrede gamle filen, derav ingen synlig feil). Løsning: full docker restart teeoff_caddy (brukeren bekreftet eksplisitt — avvek fra planens "kun graceful reload"-løfte, ga noen sekunders nedetid for teeoff.no).
    2. Alvorlig nettverksalias-kollisjon (oppdaget rett etter omstarten, da EKTE teeoff-trafikk — /api/facilities?... med ekte klubb-slugs som borregaard-golfklubb — dukket opp i teecup_api sin logg): docker-compose.yml sin service-nøkkel var api:, identisk med teeoffs eget api-servicenavn. Docker Compose registrerer nettverksalias etter service-NAVNET (ikke bare container_name) på delte nettverk, så begge containerne fikk alias apiteeoff_default — Caddys reverse_proxy api:8000 i teeoffs egen config kunne da tilfeldig treffe enten ekte teeoff_api eller teecup_api, dvs. ekte brukertrafikk til teeoff.no kunne bli besvart av TeeCup-koden. Rettet umiddelbart: stoppet teecup_api først (hindre videre feilruting), ga service-nøkkelen navnet teecup_api, gjenopprettet, bekreftet med docker network inspect at alias api nå KUN peker på ekte teeoff_api. Lærdom for fremtidige tjenester på delt nettverk: ALLTID gi docker-compose sin service-nøkkel (ikke bare container_name) et prosjekt-unikt navn når flere uavhengige compose-prosjekter deler samme eksterne nettverk — service-navnet blir også et DNS-alias.
  • Mindre driftslærdom (samlet): (a) .env leses IKKE på nytt av en allerede kjørende container — docker compose up -d --force-recreate kreves etter enhver .env-endring; (b) et #-tegn i et upassordet .env-passord kuttes som kommentar av Compose sin parser, anførselstegn (helst enkle) løser det; (c) jeg eksponerte ved et uhell to secret-verdier i eget debug-output mens jeg feilsøkte en .env-korrupsjon (manglende linjeskift) — brukeren roterte passordet som forsiktighetsregel.
  • Verifisert ende-til-ende mot den ekte, live stacken: teeoff.no upåvirket gjennom hele prosessen; https://teecup.teeoff.no/health → 200 med automatisk utstedt TLS; full magic-link-innlogging (ekte e-post mottatt, verify-link ga Secure-flagget cookie siden vi nå er over ekte https, /auth/me fungerte med sesjonen).

Ekte SMTP-utsending — BYGGET OG VERIFISERT 2026-07-16

  • Brukeren la inn egne, uavhengige SMTP-credentials i .env (TEECUP_SMTP_SERVER/PORT/USER/PASS, TEECUP_FROM_EMAIL — ADR-009, ikke delt med teeoff). Kun nøkkelnavn ble lest for å bekrefte de fantes, ALDRI verdiene (CLAUDE.md sin sikkerhetsregel).
  • Løsning: ny app/email.py (send_magic_link_email, smtplib kjørt via asyncio.to_thread siden det er et synkront bibliotek — samme mønster som teeoffs egen fungerende utsending). Håndterer BEGGE vanlige SMTP-tilkoblingsmåter dynamisk (implisitt TLS på port 465 vs. STARTTLS på andre porter) siden porten bevisst ikke ble lest under planlegging. app/config.py fikk nye, valgfrie innstillinger (SMTP_CONFIGURED avledet fra at alle fem er satt) — IKKE _required, så scratch-/dev-testing fungerer fortsatt uten SMTP satt opp, via TEECUP_DEV_LOG_MAGIC_LINKS.
  • Bevisst designvalg: en driftsfeil i selve utsendingen (feil passord, SMTP nede, eller ingen leveringsmåte konfigurert i det hele tatt) logges kun server-side og endrer ALDRI klientens respons — alt annet ville brutt anti-enumereringsgarantien i request-link (klienten skal ikke kunne skille "e-posten finnes ikke" fra "e-posten finnes men utsendingen feilet").
  • Verifisert i to trinn: (1) dev-log-flyten uendret uten SMTP satt (regresjonstest av eksisterende scratch-løype), (2) én ekte test-e-post sendt til en adresse brukeren oppga, med de ekte credentials videreført fra .env til scratch-containeren uten at jeg noensinne leste verdiene selv — brukeren bekreftet mottak av en e-post med innloggingskode. Dette er første gang noe i dette prosjektet er verifisert ved faktisk levering til en ekte, ekstern mottaker, ikke bare via curl/scratch-container.

Organisasjon-bootstrap — BYGGET OG VERIFISERT 2026-07-16

  • Gjennomgang av alle routere hadde avdekket at INGEN endepunkt opprettet en organization-rad — i alle testrunder denne økten var organisasjoner satt inn direkte med superbruker-SQL. En ekte førstegangsbruker hadde ingen vei til å opprette klubben/bedriften sin og bli owner. Reelt blokkerende, ikke en utsettbar produktbeslutning.
  • Løsning: nytt POST /orgs {"name": ...}, autorisert med get_current_user (ikke get_authorized_org — sirkulært før org-en finnes). Ingen ny migrasjon nødvendig.
  • Selvrefererende RLS-bootstrap bekreftet å fungere (kommentaren i 002 om en "privilegert sti" var ALDRI bygget og viste seg unødvendig): generer org-ens uuid i Python FØR innsetting, sett app.current_org til nøyaktig den verdien via den eksisterende org_connection(), sett så inn organization-raden med samme id. org_self-policyens implisitte WITH CHECK (id = app_current_org()) blir da trivielt sann. teecup_app (NOSUPERUSER/NOBYPASSRLS) trenger altså INGEN egen privilegert tilkobling for å bootstrappe sin egen første organisasjonsrad.
  • Verifisert med 5 tester, inkludert en negativ kontroll som beviser mekanismen er presis, ikke et RLS-hull: forsøkte å sette inn en organisasjon med en MISMATCHENDE id (annen enn app.current_org) — avvist med insufficient_privilege, som forventet. Også bekreftet: ny org fungerer normalt med eksisterende endepunkter (GET/POST tournaments), dukker opp riktig i /auth/me, og full kryss-org-isolasjon holder mellom to uavhengig opprettede organisasjoner.

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