Presiseringen din midt i byggingen (AVIF) endret arkitekturen til det bedre: droppet den opprinnelige planen med presignerte nettleser-URL-er til fordel for ekte multipart-opplasting gjennom API-et, som konverterer til AVIF (Pillow) før lagring. Dette forenklet ting betydelig — kun én MinIO-klient trengs nå, og Caddy-ruten slapp den skjøre signatur-Host-header-følsomheten presignering ville krevd. To reelle feil funnet under scratch-verifisering, aldri i produksjon: pillow-avif-plugin testet frittstående først — fungerer uten ekstra systempakker. MinIO avviser understrek i vertsnavn (teecup_minio feilet umiddelbart) — rettet til teecup-minio (bindestrek) før noe ble forsøkt mot ekte infrastruktur. Caddy-ruten unngikk en skjult DNS-felle: en ny subdomene ville pekt til en IPv6-adresse denne serveren ikke har — løst med en sti-prefiks (/teecup-media/*) på det eksisterende domenet i stedet. Verifisert i flere lag: ekte bilde lastet opp i scratch, bekreftet konvertert til gyldig AVIF, bekreftet lesbart anonymt direkte mot MinIO, alle tre valideringsveier (ugyldig format, korrupt fil, for stor fil) testet. Etter Caddy-omstart: et ekte anonymt kall mot produksjonsdomenet ga en ekte MinIO-feilrespons — beviser ruten treffer MinIO, ikke frontend sin 404-side. teeoff.no upåvirket gjennom hele runden. Bevisst ikke gjort: et ekte bilde lastet opp til en ekte, live turnering (ville skrevet i din ekte konto uten at du ble spurt) — tilbys, ikke utført. Vil du at jeg tester en ekte opplasting mot en av dine eksisterende turneringer, eller går vi videre til neste V0-skjerm (økt/program, blind draw, scorekort eller leaderboard)?
30 KiB
30 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-17
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. |
| Dato/klokkeslett på turnering/økt/match | ✅ | ADR-015. session.scheduled_at + tee_interval_minutes → utledet match.tee_time. Se egen seksjon under. |
Maskinlesbar feilkode-kontrakt ({"code",...}) |
✅ | ADR-015. Full retrofit, alle 39 tidligere steder. Se egen seksjon under. |
| i18n-forberedelse (nb/en) | ✅ | ADR-015. preferred_locale + ekte engelsk e-postmal. Se egen seksjon under. |
| Frontend: innlogging + verifisering, LIVE | ✅ | ADR-016. Next.js på teecup.teeoff.no, ekte magic-link-flyt bevist med reell e-post. Se egen seksjon under. |
| Frontend: dashboard (org-bytter/-opprettelse + turneringsliste), LIVE | ✅ | /dashboard. Kablet mot /auth/me, /orgs, /orgs/{id}/tournaments. Skrive-flyt bekreftet med ekte data (org "Tjøme Gents" + turnering opprettet av bruker). |
| Frontend: lag/roster-skjerm, LIVE | ✅ | /tournaments/[id]. To nye backend-endepunkter bygget samtidig (PATCH/DELETE roster). Skrive-flyt ikke testet med ekte data ennå. |
| Selvregistrering + utvidet spillerprofil (API) | ✅ | ADR-017. app/routers/registration.py (offentlig, uautentisert), utvidet players.py, e-post-basert player.user_id-kobling ved innlogging. Backend komplett og live. Frontend-påmeldingsskjema/landingssider gjenstår (egen ADR-runde). |
Roster: endre kaptein / fjern spiller (PATCH/DELETE) |
✅ | app/routers/tournaments.py. Bevisst ingen "kun én kaptein"-håndhevelse ennå — se «Brukerroller»-punktet under. |
Frontend: innlogging + verifisering — ✅ BYGGET OG VERIFISERT LIVE 2026-07-17
- Første frontend-skjerm i prosjektet. Designet i V0 (login-skjerm, merkevare
form/farge hentet fra Teeoff-logoen — IKKE navn/logo, se ADR-016s
begrunnelse og tidligere samtale), hentet inn som
frontend/(Next.js + Tailwind + shadcn/ui). - Kvalitetsrunde på V0-output før bruk: fargetokens i
globals.cssvar OKLCH-TILNÆRMINGER av de forespurte hex-fargene (#8bc24a/#ff5722), ikke eksakte — regnet ut presise OKLCH-ekvivalenter og rettet alle 6 forekomster (lys/mørk/system-mørk). Fjernet@vercel/analyticshelt (ga null verdi på egen-hostet infrastruktur, kun en unødvendig tredjeparts nettverkskall). Fjernettypescript: { ignoreBuildErrors: true }(bygget kjører nå ekte typesjekk). Fjernet dødtpnpm.overrides-felt.frontend/.gitignoremanglet.pnpm-store/— årsaken til at VSCode viste 10 000 "ukjente" filer ved første commit-forsøk; rettet. - Kablet mot ekte API (
app/routers/auth.pysine/auth/request-linkog/auth/verify-link):frontend/next.config.mjssinrewrites()proxyer/auth/*//orgs/*//healthserver-side til API-et — se ADR-016 for hele begrunnelsen (same-origin, ingen CORS, cookie uendret).components/login-form.tsxsender ektePOST /auth/request-link; nyapp/verify/page.tsx+components/verify-form.tsxmottar?token=...fra e-postlenken (automatisk verifisering) ELLER viser et manuelt "lim inn koden"-felt (nødvendig fallback, ikke overflødig — se under). - Backend-endring i samme runde:
app/email.py/app/config.pyfikk en nyPUBLIC_BASE_URL-innstilling (defaulthttps://teecup.teeoff.no, valgfri) — e-postmalen sender nå en EKTE klikkbar lenke ({base}/verify?token=...) i tillegg til den rå koden som fallback (samme mal-mekanisme som i18n-runden, ADR-015). - Containerisert og rullet ut LIVE (ny
frontend/Dockerfile, multi-stage, Next.jsoutput: "standalone"; nyteecup_frontend-tjeneste idocker-compose.yml). Caddy (teecup.teeoff.no, i det SEPARATEteeoff-repoet) peker nå påteecup_frontendi stedet forteecup_apidirekte — se ADR-016 for hvorfor, og CLAUDE.md-status for driftsdetaljene (samme stale-Caddy-inode-hendelse som containeriseringsrunden, løst likt). - Verifisert med FAKTISK e-postlevering, ikke bare curl: ekte
POST /auth/request-linksendt til brukerens egen adresse overhttps://teecup.teeoff.no, ekte e-post mottatt med en ekte klikkbar lenke, lenken åpnet i nettleser, landet på en fungerende/verify-side, sesjon opprettet — brukeren bekreftet "jeg er tilsynelatende innlogget." Første gang en hel bruker-vendt flyt (ikke bare API-et isolert) er bevist ende-til-ende i produksjon.
Dato/klokkeslett + feilkode-kontrakt + i18n-forberedelse — ✅ BYGGET OG VERIFISERT 2026-07-17
- Reist av brukeren rett før frontend-arbeidet: kan turnering/økt/match ha dato/klokkeslett, og må appen forberedes for flerspråklighet? Begge er API-kontraktspørsmål som er langt billigere å løse FØR frontend bygges enn å ettermontere — se ADR-015 for alle tre beslutningene i sin helhet.
- Migrasjon
006_scheduling_and_locale.sql: alt additivt (nullable/ default) —tournament.end_date(+CHECKmotstart_date),session.scheduled_at/tee_interval_minutes/start_hole,match.tee_time_override,app_user.preferred_locale,magic_link_token.locale(begge sistnevnteCHECK IN ('nb','en')). Kjørt permanent mot den ekteteecup_db(se under). - Feilkoder: ny
app_error(status_code, code, message)-factory iapp/errors.py, alle 39 tidligere norsk-hardkodedeHTTPException(..., detail="...")-steder på tvers av 6 filer (errors.py,auth.py,routers/auth.py,routers/tournaments.py,routers/matches.py,routers/scoring.py) retrofittet til en delt ~15-kode-taksonomi. Endeliggrep -rn 'detail="' app/ga NULL treff. - Dato/tid i API-et:
tournament.start_date(fantes i skjemaet siden 001, men var ALDRI koblet til API-modellene før nå — funnet og fikset som en naturlig bivirkning av å legge tilend_date) +end_dateeksponert;sessionsine tre nye felt eksponert iSessionCreate/SessionOut;match.tee_timeutledet i Python i bådecreate_matchoglist_matches(ingen ekstra spørring — økten hentes allerede for andre formål der). - i18n:
MagicLinkRequest.locale(Literal["nb","en"], defaultnb) sendes av klienten, følger med i token-raden, brukes til å velge e-postmal OG (kun ved førstegangsopprettelse)app_user.preferred_locale. Ekte engelsk e-postmal lagt inn iapp/email.py(ikke bare rørlegging) — konkret bevis på at flerspråklighet fungerer ende-til-ende, ikke bare at et felt eksisterer. - Verifisert grundig mot en fersk
teecup_scratch(migrasjoner 001→006,test_isolation.sql12/12 uendret, engangs-API-container): feilkode-form bekreftet på tvers av 5 filer (NOT_FOUND/LIMIT_REACHEDfra tournaments.py,DUPLICATEfra roster,NOT_AUTHENTICATEDuten cookie,NOT_ROSTERED_ON_TEAMfra matches.py sinadd_participant);tee_timebekreftet å øke riktig persequence(10 min intervall → 08:00/08:10/08:20),tee_time_overridebekreftet å vinne over utledet verdi, økt utenscheduled_atbekreftet å gitee_time: null(ikke feil); i18n bekreftet full runde — ny bruker medlocale:"en"fikkpreferred_locale:"en", en ETTERFØLGENDErequest-linkfor samme bruker medlocale:"nb"skiftet e-postmalen men IKKE den lagrede preferansen (bekreftet uendret via/auth/me). - Migrasjon 006 kjørt mot ekte
teecup_db2026-07-17, bruker bekreftet eksplisitt i samme økt — kjørte rent,test_isolation.sqlfortsatt 12/12. - Mindre driftslærdom, funnet OG rettet samme runde: et innledende
grep -v -i 'pass|secret|key'-filter jeg brukte for å lese ikke-sensitive.env-nøkler fanget IKKE oppTEECUP_DATABASE_URL, som barteecup_app-passordet innebygd i selve URL-en (i tillegg duplisert av det samme passordet som allerede lå rent iTEECUP_APP_PASSWORD) — passordet endte dermed synlig i et verktøyresultat. Flagget til brukeren umiddelbart (samme mønster som SMTP-passord-hendelsen under containeriseringsrunden). Rettet:.envbygget om til fem separate, rent navngitte felt (TEECUP_DB_HOST/PORT/NAME/USER/PASS),app/config.py+app/db.pybygger nåasyncpg-poolen fra disse i stedet for én DSN-streng,docker-compose.ymloppdatert tilsvarende. Se CLAUDE.md-status for full driftsdetalj, inkl. at dette krevde et fulltdocker compose up -d --build --force-recreate(ikke bare--force-recreate— imaget bygger innapp/ved build-tid). Verifisert ende-til-ende mot den live stacken (Application startup complete,/auth/mega rent401over ekte https,teeoff.noupåvirket).
Containerisering og go-live — ✅ FERDIG 2026-07-16
POST /orgsosv. var siste kodebit; dette var første gang noe rørte EKTE, PERMANENT infrastruktur (ekteteecup_db, ekte langtlevende container, den DELTE Caddy-instansen som også ruter liveteeoff.no).- Bygget: ekte
teecup_dbopprettet, migrasjoner 001→005 kjørt permanent (samme filer, ingen endringer),test_isolation.sqlbestå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.pypå samme relative plassering) +docker-compose.yml(tjenesteteecup_api, joiner det eksisterende, eksterneteeoff_default-nettverket). Ny Caddy-blokk forteecup.teeoff.noi/opt/teeoff/deploy/Caddyfile. - To reelle driftshendelser, begge funnet og rettet i sanntid:
- Stale bind-mount-inode:
teeoff_caddysinCaddyfile-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 gangercaddy validate/reload/admin-API/loadble kjørt (alle opererte på den uendrede gamle filen, derav ingen synlig feil). Løsning: fulldocker restart teeoff_caddy(brukeren bekreftet eksplisitt — avvek fra planens "kun graceful reload"-løfte, ga noen sekunders nedetid forteeoff.no). - Alvorlig nettverksalias-kollisjon (oppdaget rett etter omstarten, da
EKTE teeoff-trafikk —
/api/facilities?...med ekte klubb-slugs somborregaard-golfklubb— dukket opp iteecup_apisin logg):docker-compose.ymlsin service-nøkkel varapi:, identisk med teeoffs egetapi-servicenavn. Docker Compose registrerer nettverksalias etter service-NAVNET (ikke barecontainer_name) på delte nettverk, så begge containerne fikk aliasapipåteeoff_default— Caddysreverse_proxy api:8000i teeoffs egen config kunne da tilfeldig treffe enten ekteteeoff_apiellerteecup_api, dvs. ekte brukertrafikk til teeoff.no kunne bli besvart av TeeCup-koden. Rettet umiddelbart: stoppetteecup_apiførst (hindre videre feilruting), ga service-nøkkelen navnetteecup_api, gjenopprettet, bekreftet meddocker network inspectat aliasapinå KUN peker på ekteteeoff_api. Lærdom for fremtidige tjenester på delt nettverk: ALLTID gi docker-compose sin service-nøkkel (ikke barecontainer_name) et prosjekt-unikt navn når flere uavhengige compose-prosjekter deler samme eksterne nettverk — service-navnet blir også et DNS-alias.
- Stale bind-mount-inode:
- Mindre driftslærdom (samlet): (a)
.envleses IKKE på nytt av en allerede kjørende container —docker compose up -d --force-recreatekreves 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.noupåvirket gjennom hele prosessen;https://teecup.teeoff.no/health→ 200 med automatisk utstedt TLS; full magic-link-innlogging (ekte e-post mottatt,verify-linkgaSecure-flagget cookie siden vi nå er over ekte https,/auth/mefungerte 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,smtplibkjørt viaasyncio.to_threadsiden 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.pyfikk nye, valgfrie innstillinger (SMTP_CONFIGUREDavledet fra at alle fem er satt) — IKKE_required, så scratch-/dev-testing fungerer fortsatt uten SMTP satt opp, viaTEECUP_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
.envtil 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 medget_current_user(ikkeget_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_orgtil nøyaktig den verdien via den eksisterendeorg_connection(), sett så innorganization-raden med samme id.org_self-policyens implisitteWITH 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 medinsufficient_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_isolationpå 14 tabeller +org_selfpåorganization) bruktecurrent_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 viaSET LOCALkunne 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
STABLESQL-funksjonapp_current_org()(migrasjon005_rls_null_guard.sql) som gjørNULLIF(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 itest_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 medorg_connection(), deretter kall/auth/mepå samme gjenbrukte tilkobling — gikk fra 500 til 200). - Viktig presisering oppdaget underveis: den opprinnelige planen antok at
/auth/mekunne joineorganizationdirekte 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 atorg_self-policyen krever en MATCHENDEapp.current_orgfor å 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/meslår derfor opp hvert org-navn ETT OM GANGEN viaorg_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_captainpå 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.
- Nytt 2026-07-18:
PATCH .../roster/{id}(sette/fjerne kaptein) håndhever bevisst IKKE «kun én kaptein per lag» — flere spillere kan i dag merkes kaptein samtidig på samme lag. Bør revurderes samtidig med resten av dette punktet, ikke løses isolert i roster-endepunktet.
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_scoreogsubmit_hole_result(app/routers/scoring.py) avviser nå med 409 («Matchen er avgjort og kan ikke lenger endres.») FØR upserten kjøres, hvismatch.points_side_a IS NOT NULL— dette er allerede et pålitelig, entydig signal siden kolonnen kun settes nårcompute_match_statesier 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 etstatus-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 (seapp/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-scoresforstroke-modus,hole-resultsforhole_result-modus, begge mater sammecompute_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å fortsatt ikke besluttet for FEED-en spesifikt, men mekanismen finnes nå: tournament.visibility + get_current_user_optional/_is_participant() (ADR-018) er bygget og live — gjenbruk dette, ikke bygg en ny mekanisme. |
| 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. |
Landingssider (turnering + organisasjon) — ADR-018 ✅ HELT FERDIG 2026-07-18
Reist av brukeren 2026-07-18, rett etter registrerings-ADR-en (ADR-017). Backend, begge frontend-skjermer, Open Graph-metadata OG MinIO-bilde- opplasting (med AVIF-konvertering) bygget og live samme dag. Kun selve dra-og-slipp-opplastingsskjermen i frontend (V0) gjenstår.
| Del | Status | Notat |
|---|---|---|
| Synlighetsnivå: offentlig / kun org-medlemmer / kun turnering-deltakere | ✅ | tournament.visibility (default org, trygg standard) + organization.public_profile. Håndheves eksplisitt i registration.py — RLS løser IKKE dette alene (se ADR-018 Beslutning B, reell presisering funnet under bygging). Samme trenivå-modell som «Banter Board» under bør gjenbruke dette. |
| Registrering følger samme synlighetsgrense | ✅ | ADR-018 Beslutning D, bekreftet med bruker FØR bygging. |
| "Deltaker"-tilgang (ikke org-medlem, men rostret/registrert) | ✅ | Ny get_current_user_optional + _is_participant(). Testet: rostret spiller som logget inn fikk tilgang, tilfeldig fremmed ble avvist. |
| Turnering-landingsside: tekst, program, sponsorer, påmelding (API) | ✅ | description-felt, tournament_sponsor-tabell (navn+lenke), GET /public/tournaments/{id}/sessions (gjenbruker blind draw-lås fra ADR-013). Selve SKJERMEN i frontend gjenstår. |
| Org-landingsside: klubbprofil, liste over turneringer (API) | ✅ | GET /public/orgs/{slug} — kun public-synlige turneringer. Bekreftet: klubb-profil KAN være offentlig (brukerens valg). |
| Lesbar URL (slug) for organisasjon | ✅ | Fantes faktisk allerede i skjemaet siden migrasjon 001 (oversett, funnet da migrasjon 009 feilet mot scratch — se CLAUDE.md-status). Kun CHECK-constraints lagt til i 009. |
| Organisator kan faktisk SETTE disse feltene | ✅ | Implisitt hull fylt under bygging: PATCH /orgs/{id}/tournaments/{id} (visibility/description/registrering), PATCH /orgs/{id} (slug/public_profile), full sponsor-CRUD. |
| Bilder (hero, sponsorlogoer), backend | ✅ | Ny teecup-minio-tjeneste, ekte multipart-opplasting → AVIF-konvertering (Pillow) → lagring, live på teecup.teeoff.no/teecup-media/*. Selve opplastingsskjermen i frontend (V0) gjenstår. |
| Del-metadata (Open Graph: og:title/og:description) | ✅ | generateMetadata() på /t/[id]+/clubs/[slug], ekte data fra API-et, verifisert mot produksjonsimaget. og:image gjenstår (MinIO). |
| Blind draw-skjuling på offentlig side | ✅ | Arves automatisk via delt _fetch_sessions()-hjelpefunksjon (ADR-018 Beslutning E) — ikke reimplementert. |
| Antall påmeldte / ledige plasser vist åpent | ✅ | confirmed_count i GET /public/tournaments/{id}. |
| Frontend: turnering-landingsside + påmeldingsskjema, LIVE | ✅ | /t/[id]. Tre bekreftelsestilstander (bekreftet/venteliste/godkjenning venter). Verifisert med ekte POST-registrering mot scratch. |
| Frontend: org-/klubb-landingsside, LIVE | ✅ | /clubs/[slug]. Gjenbruker TournamentCard (nå med valgfri orgId) for turneringslisten. Verifisert: viser kun public-synlige turneringer. |
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).