diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index d83eaff..76ff1ec 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -276,7 +276,8 @@
"Bash(grep -v '^$')",
"Bash(grep -rl \"POSTGRES_PASSWORD\" /opt/teeoff --include='*.yml' --include='*.env*' 2>/dev/null)",
"Bash(grep -n \"^ [a-zA-Z0-9_-]*:$\" /opt/teeoff/docker-compose.prod.yml)",
- "Bash(set -e)"
+ "Bash(set -e)",
+ "Bash(curl -s -o /dev/null -w '%{http_code}\\\\n' https://teecup.teeoff.no/dashboard)"
],
"additionalDirectories": [
"/opt/teeoff/deploy",
diff --git a/CLAUDE.md b/CLAUDE.md
index d92daee..ae2168c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1068,15 +1068,139 @@ Ferdig og verifisert:
ekte typesjekket build. Rullet ut live, ren frontend-endring, `teeoff.no`
upåvirket.
+- **Invitasjonskode + ledende side + projisert stilling, BACKEND LIVE
+ (2026-07-19, ADR-020):** brukeren reiste tre relaterte hull rett etter at
+ "bygg i rekkefølgen ting brukes"-serien var ferdig: (1) ingen vei inn til
+ en turnering for en spiller som bare har fått muntlig beskjed, (2)
+ leaderboardet viser kun faktisk opptjente poeng, ikke hva stillingen ville
+ blitt om pågående matcher holder seg, (3) ingen fargekoding i matchlister
+ for hvem som leder. Full ADR-020 skrevet (4 delbeslutninger, se
+ ARCHITECTURE_DECISIONS.md) — nøkkelbeslutning bekreftet eksplisitt av
+ bruker FØR bygging: en invitasjonskode OVERSTYRER `tournament.visibility`
+ helt (koden ER selve invitasjonen, ikke en snarvei som fortsatt krever
+ eksisterende tilgang).
+ Ny migrasjon `011_join_code_and_leading_side.sql`: `tournament.join_code`
+ (6 tegn, alfabet uten 0/O/1/I, globalt unikt, backfylt for eksisterende
+ rader), `match.leading_side` (cachet fortegn av `MatchState.lead`, samme
+ mønster som `status_text`/`points_side_a/b`), fjerde
+ `SECURITY DEFINER`-bro `public_tournament_by_code()` (etter
+ `public_tournament_org` 007, `link_player_by_email` 008,
+ `public_org_by_slug` 009).
+ **Backend bygget:** `create_tournament` genererer koden (retry-løkke ved
+ kollisjon, astronomisk usannsynlig med 33^6 kombinasjoner). Ny
+ `GET /public/tournaments/by-code/{code}` (MÅ registreres FØR
+ `/{tournament_id}` i routeren, ellers tolkes "by-code" som en ugyldig
+ UUID). `GET /public/tournaments/{id}`, `GET .../sessions` og
+ `POST .../register` godtar alle en valgfri `code`-parameter som — når den
+ matcher — hopper `_check_visibility()` helt over.
+ `recompute_and_cache_match_state` (scoring.py) cacher nå `leading_side`
+ ved HVER hull-innsending, ikke bare ved avgjørelse. Leaderboard-
+ endepunktet fikk `projected_points`/`projected_points_by_team`:
+ avgjorte matcher bidrar likt til faktisk og projisert, ikke-avgjorte gir
+ hele `points_per_match` til `leading_side` (delt 0,5/0,5 ved "AS"/ikke
+ startet) — speiler hvordan Ryder Cup-TV-dekning viser "hvis det sluttet
+ nå".
+ **Reell hendelse underveis, håndtert transparent (ikke skjult):** en
+ feilformulert `docker exec teeoff_db env | grep -i POSTGRES`-kommando
+ (ment å liste variabelNAVN) fanget opp `POSTGRES_PASSWORD` sin VERDI også,
+ siden selve nøkkelnavnet matchet søkemønsteret — eksponerte `teeoff_db`
+ sitt superbruker-passord (`teeoff_admin`) i verktøyresultatet. Alvorligere
+ enn de to tidligere passord-hendelsene i prosjektet siden dette er
+ superbrukeren for HELE den delte Postgres-klyngen (teeoff OG teecup), ikke
+ en enkelt tjeneste-credential. Flagget til bruker umiddelbart, som valgte
+ å rotere. Rotert trygt UTEN å noensinne re-eksponere gammel ELLER ny verdi
+ i noe synlig kommandoresultat: `ALTER ROLE` kjørt via lokal
+ Unix-socket-`trust`-auth (bekreftet ved å lese `pg_hba.conf`, ingen
+ hemmelighet involvert i den sjekken) — krevde altså IKKE det gamle
+ passordet i det hele tatt. Nytt passord generert med `openssl rand -hex
+ 32` (hex, ikke base64 — unngår SAMME klasse URL-enkodings-felle som
+ `TEECUP_DATABASE_URL`-hendelsen tidligere, siden verdien også ligger i en
+ `postgres://`-DSN). `/opt/teeoff/.env` sine TO forekomster
+ (`POSTGRES_PASSWORD` og `DATABASE_URL`) oppdatert med `sed`-mønstre som
+ ALDRI leser/skriver ut den gamle verdien. `teeoff_api`/`teeoff_worker`
+ service-nøklene i `docker-compose.prod.yml` viste seg å hete `api`/
+ `worker` (ikke `teeoff_api`/`teeoff_worker` — det er kun
+ `container_name`), samme "service-nøkkel ≠ container-navn"-fallgruve som
+ nettverksalias-hendelsen fra containeriseringsrunden. `docker compose up
+ -d --force-recreate api worker` gjenskapte OGSÅ `teeoff_db` selv (ikke
+ eksplisitt navngitt) — Compose oppdager konfigurasjonsendring
+ (`${POSTGRES_PASSWORD}` i `db`-tjenestens egen `environment:`) og
+ gjenskaper uansett hvilke tjenester som ble navngitt. Verifisert grundig
+ ETTERPÅ: `teeoff_db`-loggen viste "Skipping initialization" (datavolum
+ urørt, ikke reinitialisert) + ren oppstart, `teeoff_api`/`teeoff_worker`
+ ren oppstart uten en eneste feil-/auth-/passord-linje i hele loggen,
+ `teeoff.no` OG `teecup.teeoff.no/health` begge `200` etterpå.
+ **Scratch-verifisert grundig** (fersk `teecup_scratch` 001→011,
+ isolert `teecup_app_scratch`-rolle, isolert scratch-MinIO, engangs
+ API-container): `test_isolation.sql` 12/12 (måtte først rettes — tre
+ RÅ `INSERT INTO tournament`-steder i selve testfilen predaterte
+ `join_code` og traff den nye NOT NULL-constrainten, rettet med
+ dummy-koder). Full ende-til-ende-runde: to turneringer opprettet, ulike
+ koder bekreftet; `by-code`-oppslag bekreftet for kjent OG ukjent kode
+ (404); anonym lesing av en `org`-synlig turnering BLOKKERT uten kode
+ (403 NOT_VISIBLE), TILLATT med riktig kode (inkl. case-insensitivt),
+ FORTSATT blokkert med feil kode; samme mønster bekreftet for
+ `POST .../register`. Full hull-for-hull-simulering av en singel-match
+ (hole_result-modus): `leading_side`/`status_text` fulgte hverandre
+ eksakt gjennom "1 UP (A)" → "AS" (leading_side=null) → avgjort "9&7
+ (A)", leaderboardets `projected_points` traff nøyaktig 1.0/0.0 mens A
+ ledet, 0.5/0.5 ved "AS", og ble likt `points`/`projected_points` (begge
+ 1.0/0.0) etter avgjørelse.
+ **Rullet ut mot ekte `teecup_db` 2026-07-19**, bruker bekreftet
+ eksplisitt: migrasjon 011 kjørt (eneste eksisterende turnering fikk
+ automatisk generert kode), `test_isolation.sql` fortsatt 12/12, kun
+ `teecup_api` redeployet (ingen frontend-endring i denne del-runden),
+ `teeoff.no` upåvirket.
+ **Frontend fullført samme dag, egen del-runde:** `login-form.tsx` fikk et
+ eget kode-modus (`JoinByCode`) — «Har du en invitasjonskode?»-lenke bytter
+ ut e-post-skjemaet, slår opp `/public/tournaments/by-code/{code}` og
+ navigerer til `/t/{id}?code=...` med Next sin `useRouter`. `code`
+ query-param tres gjennom hele veien: `app/t/[id]/page.tsx` leser
+ `searchParams`, `public-tournament.tsx` sender den med på BÅDE
+ info-/sessions-lesingen og selve `POST .../register` (ikke bare det
+ første oppslaget som tok deg dit).
+ `tournament-detail.tsx` viser koden i en egen kopier-chip i headeren —
+ ingen enkelt-turnering-`GET` fantes, så komponenten henter i stedet hele
+ org-ens turneringsliste (som allerede bærer `join_code`) og finner egen
+ rad, i stedet for å legge til et nytt endepunkt kun for dette.
+ `tournament-leaderboard.tsx` fikk en ny `SegmentedBar`-komponent — ETT
+ fargesegmentert rektangel per bar (ikke tall side om side), proporsjonalt
+ med hvert lags poeng, 50/50 nøytralt ved 0-0. To slike bares rett under
+ headeren: "Stilling nå" (faktisk) og "Projisert (hvis pågående matcher
+ holder seg)" — den EKSISTERENDE store tall-scoreboarden beholdt uendret
+ lenger ned som detaljvisning.
+ `session-blind-draw.tsx` sin `RevealedView`: matchkortet får nå en farget
+ toppkant (leaderens `team.color`) og en `status_text`-chip (fylt farge
+ ved avgjort match m/ konfetti-ikon, lys tone ved pågående) i stedet for
+ kun klokkeslettet; `RevealSide` for ledende side får en svak fargetonet
+ bakgrunn. `leading_side`/`status_text`/`points_side_a/b` lagt til
+ `ApiMatch`-typen (var der allerede i API-et, bare ikke konsumert
+ frontend-siden før nå).
+ **Verifisert:** ekte typesjekket produksjonsbuild (samme `Dockerfile`
+ som deployes, ikke dev-server) kompilerte rent, alle 10 ruter listet.
+ Rullet ut (kun `teecup_frontend`, ingen backend-endring i denne delen),
+ `teecup.teeoff.no/dashboard` og `/` → 200, `teeoff.no` upåvirket. Ekte
+ smoke-test i produksjon: `by-code`-oppslag for "De Gamle er Eldst" sin
+ faktiske kode ga riktig turnering-id, `/t/{id}?code=...` ga 200.
+ **ADR-020 er dermed helt ferdig** (backend + frontend, alle fire
+ del-ønsker: kode-basert oppdagelse, kode-felt på login, projisert
+ stilling, fargekoding av matcher) — bortsett fra kode-regenerering, som
+ er bevisst utsatt (se FEATURE_BACKLOG.md).
+ **Reell driftshendelse underveis** (mellom backend- og frontend-delen,
+ under scratch-oppsett): en feilformulert `grep -i POSTGRES`-kommando
+ eksponerte `teeoff_db` sitt superbruker-passord ved et uhell. Flagget
+ umiddelbart, brukeren valgte å rotere — se detaljene under
+ backend-avsnittet over for hele hendelsen og hvordan roteringen ble
+ gjennomført uten å noensinne re-eksponere gammel eller ny verdi.
+
Neste steg:
1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
2. Kommunikasjon (chat/feed) — ikke startet.
3. Fortsatt åpne beslutninger fra FEATURE_BACKLOG.md (walkover/konsesjon,
- sanntid/WebSockets vs. polling, tilskuer-rolle) — bør avklares i egne,
- dedikerte runder når det er naturlig.
- design i V0 (fortsett i samme prosjekt), FORVENT en full re-eksport hver
- gang — diff mot live-treet i et scratch-område før noe pakkes ut over
- eksisterende filer, og sjekk om V0-skjermen bygger inn handlinger backend
- ikke støtter ennå FØR integrering.
-3. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
-4. Kommunikasjon (chat/feed) — ikke startet.
+ sanntid/WebSockets vs. polling, tilskuer-rolle, kode-regenerering for
+ ADR-020) — bør avklares i egne, dedikerte runder når det er naturlig.
+4. Ved fremtidige nye V0-skjermer/-design i V0 (fortsett i samme prosjekt),
+ FORVENT en full re-eksport hver gang — diff mot live-treet i et
+ scratch-område før noe pakkes ut over eksisterende filer, og sjekk om
+ V0-skjermen bygger inn handlinger backend ikke støtter ennå FØR
+ integrering.
diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md
index 06064fd..efefa23 100644
--- a/FEATURE_BACKLOG.md
+++ b/FEATURE_BACKLOG.md
@@ -646,6 +646,28 @@ sammenslåing eller rekkefølge-endring i en senere runde.
---
+## Invitasjonskode, ledende side og projisert stilling — ADR-020
+
+Reist av brukeren 2026-07-19, rett etter at kamp-play-flyten var komplett.
+Full design i ARCHITECTURE_DECISIONS.md ADR-020.
+
+| Del | Status | Notat |
+|---|---|---|
+| Kort invitasjonskode per turnering, overstyrer visibility | ✅ backend | `tournament.join_code` (migrasjon 011), genereres automatisk ved opprettelse. `GET /public/tournaments/by-code/{code}` + kode-bypass i `registration.py`. Løser "muntlig invitert spiller finner ikke turneringen"-hullet. |
+| `match.leading_side` (strukturert, ikke tekst-parsing) | ✅ backend | Cachet i `recompute_and_cache_match_state` ved hver hull-innsending, eksponert på `MatchOut`. |
+| Projisert stilling (hvis pågående matcher holder seg) | ✅ backend | `TeamStanding.projected_points` + `SessionStanding.projected_points_by_team` i leaderboard-endepunktet. Ikke-avgjorte matcher gir full poengsum til `leading_side`, delt 0,5/0,5 ved "AS". |
+| Login-skjerm: kode-felt, tar deg direkte til turneringen | ✅ LIVE 2026-07-19 | `login-form.tsx` sin `JoinByCode`. `code` tres gjennom `/t/[id]` sin lesing OG registrering. |
+| Turnering-detalj: vis invitasjonskode (kopier-knapp) | ✅ LIVE 2026-07-19 | `tournament-detail.tsx` sin `JoinCodeChip` — henter fra org-ens turneringsliste, ikke et nytt endepunkt. |
+| Leaderboard: to stillingsbarer (faktisk + projisert) | ✅ LIVE 2026-07-19 | `tournament-leaderboard.tsx` sin `SegmentedBar` — fargesegmentert rektangel, ikke tall side om side. Eksisterende tall-scoreboard beholdt som detaljvisning. |
+| Fargekoding av matchlister etter ledende lag | ✅ LIVE 2026-07-19 | `session-blind-draw.tsx` sin `RevealedView`/`RevealSide` — farget toppkant + status-chip, bruker `leading_side` + eksisterende `team.color`, ingen ny fargemodell. |
+| Kode-regenerering (ved lekket kode) | 💤 bevisst utsatt | Ikke bygget denne runden — ingen organisator-vei til å bytte ut en kode ennå. Egen sak hvis etterspurt. |
+
+**ADR-020 er dermed helt ferdig** — backend + frontend, alle fire
+del-ønsker levert samme dag. Se CLAUDE.md-status for full runde inkl. en
+reell (og transparent håndtert) passord-eksponeringshendelse underveis.
+
+---
+
## UX / frontend (senere fase)
- 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp
diff --git a/frontend/app/t/[id]/page.tsx b/frontend/app/t/[id]/page.tsx
index b592178..3975ad3 100644
--- a/frontend/app/t/[id]/page.tsx
+++ b/frontend/app/t/[id]/page.tsx
@@ -44,9 +44,12 @@ export async function generateMetadata({
export default async function PublicTournamentPage({
params,
+ searchParams,
}: {
params: Promise<{ id: string }>
+ searchParams: Promise<{ code?: string }>
}) {
const { id } = await params
- return
+ const { code } = await searchParams
+ return
}
diff --git a/frontend/components/login-form.tsx b/frontend/components/login-form.tsx
index eb1ff94..41cc9bc 100644
--- a/frontend/components/login-form.tsx
+++ b/frontend/components/login-form.tsx
@@ -2,7 +2,8 @@
import type React from "react"
import { useEffect, useRef, useState } from "react"
-import { Mail, ArrowLeft, CheckCircle2 } from "lucide-react"
+import { useRouter } from "next/navigation"
+import { Mail, ArrowLeft, CheckCircle2, KeyRound } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
@@ -14,6 +15,7 @@ function isValidEmail(value: string) {
}
export function LoginForm() {
+ const [mode, setMode] = useState<"email" | "code">("email")
const [email, setEmail] = useState("")
const [touched, setTouched] = useState(false)
const [sent, setSent] = useState(false)
@@ -83,6 +85,8 @@ export function LoginForm() {
onResend={handleResend}
onReset={handleReset}
/>
+ ) : mode === "code" ? (
+ setMode("email")} />
) : (