Bygget: GET /public/tournaments/{id} og POST /public/tournaments/{id}/register — helt uautentisert, egen /public-prefiks. players.py utvidet med alle sju nye feltene.
Grundig scratch-testet, ikke bare "kjørte uten feil": samtykke-avvisning, duplikat-avvisning, e-post-matching mot en organisator-forhåndsopprettet spiller (bekreftet ingen duplikat, mobil fylt inn, navn ikke overskrevet), kapasitet+venteliste, kapasitet+stengt, godkjenningskrav, utløpt frist — alle seks scenarioene fra ADR-en testet én etter én og ga riktig resultat.
Notatet ditt om synlighet er fanget i FEATURE_BACKLOG.md, koblet til det samme åpne spørsmålet for «Banter Board»-feeden — før dette API-et ble bygget, ikke etter, slik du ba om.
Gjenstår, bevisst utsatt:
E-post-basert kontosammenkobling ved innlogging (ADR-017 Beslutning B sin andre halvdel) — trenger en ny SECURITY DEFINER-funksjon på tvers av org-er, altså migrasjon 008 siden 007 alt er kjørt mot prod. Ikke gjort i denne runden.
Selve påmeldingsflyten er ikke testet med ekte data mot prod (kun ikke-destruktive sjekker: ukjent turnering ga korrekt 404).
Landingssider — egen ADR-runde, som avtalt.
29 KiB
29 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. 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å ikke besluttet (deltakere/org/offentlig lenke) — SAMME trenivå-modell som landingssider under, bør trolig løses med ÉN delt mekanisme, ikke to separate. |
| 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) — idé, ikke besluttet
Reist av brukeren 2026-07-18, rett etter registrerings-ADR-en (ADR-017). Egen ADR-runde planlagt SENERE — for mange åpne spørsmål til å bygges sammen med registrerings-API-et.
| Del | Status | Notat |
|---|---|---|
| Synlighetsnivå: offentlig / kun org-medlemmer / kun turnering-deltakere | ❓ | Eksplisitt krav fra brukeren, må ikke glemmes. Samme trenivå-modell som «Banter Board» over — bør trolig dele mekanisme, ikke løses to ganger. Sannsynligvis et felt på turnering (og org?) som styrer hvilken av de tre gruppene som får se siden. |
| Turnering-landingsside: hero, tekst, program, sponsorer, påmelding | 📋 | Innhold bør endre seg med turneringens livssyklus (meld deg på → følg live → se resultater) — samme URL, ulikt hovedbudskap avhengig av dato/status. |
| Org-landingsside: klubbprofil, liste over turneringer | ❓ | Uavklart om dette i det hele tatt skal være en offentlig, søkbar profil, eller bare en intern hub med delbare turneringslenker. |
| Lesbar URL (slug) for organisasjon | 📋 | Trengs for at en delt lenke skal se ut som noe («teecup.teeoff.no/tjome-gents»), ikke en UUID. Nytt felt på organization. |
| Bilder (hero, sponsorlogoer) | 📋 | Krever objektlagring (MinIO) — arkitektur-invarianten finnes alt (se CLAUDE.md), men MinIO er ALDRI satt opp i praksis. Første funksjon som faktisk trenger det. |
| Del-metadata (Open Graph: og:title/og:image/og:description) | 📋 | Lett å glemme, høy verdi — dette ER siden folk faktisk deler i WhatsApp/e-post. |
| Blind draw-skjuling på offentlig side | 📋 | Må respektere ADR-013 (skjult oppstilling til begge lag er låst) akkurat som resten av appen — en offentlig side må IKKE lekke dette ved et uhell. |
| Antall påmeldte / ledige plasser vist åpent | 📋 | Bruker tournament.registration_capacity fra ADR-017 direkte, ingen ny data nødvendig. |
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).