Live nå — kun teecup_frontend redeployet, begge containere friske, teeoff.no upåvirket. Statusdokumentene er oppdatert med rotårsaken (nestet <form> → nettleseren slo sammen skjemaene → «Søk» submittet det ytre økt-skjemaet som ekte sidenavigasjon → vasket bort org-parameteren).

This commit is contained in:
Erol Haagenrud 2026-07-18 15:54:40 +02:00
parent b28f604fe9
commit 0b6e7e4219
4 changed files with 118 additions and 10 deletions

View file

@ -247,7 +247,8 @@
"Bash(awk -F: '$1>=782 && $1<=980')", "Bash(awk -F: '$1>=782 && $1<=980')",
"Bash(python3 -)", "Bash(python3 -)",
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(len\\(d\\), 'facilities'\\); print\\(d[:2]\\)\")", "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(len\\(d\\), 'facilities'\\); print\\(d[:2]\\)\")",
"Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/scratch_cookies2.txt)" "Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/scratch_cookies2.txt)",
"Bash(curl -s \"https://teecup.teeoff.no/tournaments/73b6bf38-90cd-4ef7-9535-8b8e108eaa9a?org=test-org-id&name=Test\")"
], ],
"additionalDirectories": [ "additionalDirectories": [
"/opt/teeoff/deploy", "/opt/teeoff/deploy",

View file

@ -732,11 +732,54 @@ Ferdig og verifisert:
prinsipp som handicap-snapshotten (ADR-007). Server-til-server-kall prinsipp som handicap-snapshotten (ADR-007). Server-til-server-kall
(`teecup_api` → `http://teeoff_api:8000`, internt Docker-nettverk, ingen (`teecup_api` → `http://teeoff_api:8000`, internt Docker-nettverk, ingen
auth trengs). Ny migrasjon `010` (unik `external_course_ref` per org, auth trengs). Ny migrasjon `010` (unik `external_course_ref` per org,
hindrer dupliserte importer). Bygges nå — se ADR-019 for alle fem hindrer dupliserte importer). Se ADR-019 for alle fem delbeslutningene.
delbeslutningene. **Bygget, scratch-verifisert MOT EKTE `teeoff_api`** (ikke en simulert
respons — ekte HTTP-kall til den kjørende produksjonscontaineren, kun
lesing): søkte opp "Borregaard" i teeoff sine 174 publiserte anlegg,
hentet Borregaard Golfklubb sin hovedbane (18 hull, 4 tee-farger ×
kjønn), importerte den til en scratch-org — alle 18 hull med riktig
par/stroke-index (1-18, unik), alle 8 tee/tee_rating-rader med riktig
full_18 course/slope-rating verifisert direkte i databasen, opprettet
deretter en ekte økt med den importerte banen som `course_id` (beviser
hele veien til handicap-motoren fungerer, ikke bare selve importen).
Reimport av samme bane korrekt avvist (409 DUPLICATE, migrasjon 010 sin
indeks). Kryss-org-isolasjon bekreftet, ukjent teeoff-slug ga korrekt 404,
`test_isolation.sql` 12/12. Ekte typesjekket produksjonsbuild av
frontend-utvidelsen (bane-søk i program-skjemaet: søk anlegg → velg bane →
importer, samme UI-mønster som spiller-/bane-type-ahead ellers i appen).
**Rullet ut live 2026-07-18**, bruker bekreftet eksplisitt: migrasjon 010
kjørt mot ekte `teecup_db` (kun én ny partiell unik-indeks, ingen
eksisterende rader rørt), begge containere bygget+redeployet, live
sjekker OK, `teeoff.no` upåvirket. **"Bygg i rekkefølgen ting brukes"-
planen gjenopptas nå** — blind draw-skjermen er neste steg.
**Reell produksjonsbug funnet OG fikset samme dag, rapportert av bruker
som faktisk brukte funksjonen:** brukeren klikket seg korrekt via
dashbord → turnering → Program-fane (bekreftet med skjermbilde + full
klikk-sti, ikke gjettet), åpnet "Hent bane fra teeoff", søkte "Tjøme", og
fikk feilmeldingen "Mangler organisasjon i lenken" — URL-en hadde da
MISTET `?org=...&name=...`. Root-cause: `OfficialCourseSearch` sitt eget
søke-`<form onSubmit={runSearch}>` var rendret INNI `CreateSessionCard`
sitt eksisterende `<form onSubmit={handleSubmit}>` -- nestede
`<form>`-elementer er ugyldig HTML. Nettleseren slår sammen de to
skjemaene i den faktiske DOM-en, så "Søk"-knappen submittet i praksis det
YTRE økt-skjemaet som en ekte native side-navigasjon (GET til gjeldende
sti, ingen navngitte felt => tom spørrestreng) -- dette vasket bort
org-parameteren og landet brukeren på siden sin egen org-guard. **Fikset**
ved å fjerne det indre `<form>`-elementet helt (vanlig `<div>` +
Enter-tast-håndtering på inputet + `type="button"` i stedet for
`type="submit"` på søkeknappen) -- gjør nestede skjemaer strukturelt
umulig fremover for denne komponenten. Ekte typesjekket
produksjonsbuild kjørt på nytt, kun `teecup_frontend` redeployet (ingen
backend-endring). **Lærdom for fremtidige skjermer:** en ny
søk-/underskjema-widget som skal plasseres INNI et eksisterende skjema
(slik som denne bane-søk-widgeten ligger inni økt-opprett-skjemaet) må
ALDRI være et eget `<form>` -- bruk `<div>` + eksplisitt
klikk-/Enter-håndtering.
Neste steg: Neste steg:
1. Flere V0-skjermer (blind draw, scorekort, leaderboard) — samme mønster: 1. Blind draw-skjermen (neste i "bygg i rekkefølgen ting brukes", gjenopptatt
etter ADR-019-sidespranget) — deretter scorekort, leaderboard. Samme
mønster:
design i V0 (fortsett i samme prosjekt), FORVENT en full re-eksport hver 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 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 eksisterende filer, og sjekk om V0-skjermen bygger inn handlinger backend

View file

@ -25,7 +25,7 @@
| Dedikert app-rolle (ingen superuser/BYPASSRLS) | ✅ | migrasjon 002. | | 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: 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. | | 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 → **ADR-019** (design besluttet 2026-07-18, bygges nå — sidesprang fra "bygg i rekkefølgen ting brukes", blind draw-skjermen er fortsatt neste steg etterpå). Import (kopi) ved eksplisitt organisator-valg, ikke live oppslag — se ADR-019 i ARCHITECTURE_DECISIONS.md for alle fem delbeslutningene og CLAUDE.md-status for kartleggingen av teeoff sitt `/api/facilities`-API. | | Banedata fra teeoff via API | ✅ | ADR-004 → **ADR-019**, LIVE 2026-07-18. Import (kopi) ved eksplisitt organisator-valg, ikke live oppslag ved hver bruk. Se egen seksjon under. |
| Konfigurerbar handicap-pipeline (4 brytere) | ✅ | ADR-014. Bygget i `app/handicap.py`, brukt av scoring-runden. | | 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 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. | | RLS-tomstreng-fiks (`app_current_org()`) | ✅ | Migrasjon `005_rls_null_guard.sql`. Se detaljer under. |
@ -502,6 +502,52 @@ andre skjermen når den ble generert i en egen prompt.
--- ---
### Offisiell banedata fra teeoff — ✅ BYGGET OG LIVE 2026-07-18 (ADR-019)
Bevisst sidesprang fra "bygg i rekkefølgen ting brukes" rett etter
program-skjermen: brukeren påpekte at ADR-004s teeoff-integrasjon fortsatt
bare var vedtatt, ikke bygget. Full design i ARCHITECTURE_DECISIONS.md
ADR-019 (fem delbeslutninger). Kort: organisator søker blant teeoff sine
baner i program-skjemaet, velger én, og teecup KOPIERER bane+hull+tee+
tee_rating inn i `teecup_db` (`source='official'`) — ikke et live oppslag
ved hver bruk. Ny `app/teeoff_client.py` (ren HTTP-klient mot
`http://teeoff_api:8000`, internt Docker-nettverk, ingen auth trengs — begge
containere deler allerede `teeoff_default`). To nye endepunkter i
`app/routers/courses.py`: `GET .../courses/official-search[/{slug}]` og
`POST .../courses/official-import`. Migrasjon `010` (unik
`external_course_ref` per org, hindrer dupliserte importer).
**Bevisste avgrensninger for denne runden:** kun 18-hulls baner kan
importeres (teeoffs skjema har ingen egen 9-hulls-inndeling); kun
`full_18`-rating importeres (teeoff har ingen separat front9/back9-rating,
samme valg som ADR-008 allerede tok for egendefinerte baner); ufullstendige
teeoff-data (manglende par/hcp_index på et hull, eller en tee uten NOEN
rating) avviser hele importen tydelig (`EXTERNAL_DATA_INCOMPLETE`) FØR noe
skrives, ikke en delvis importert bane.
**Verifisert grundig, inkludert mot EKTE `teeoff_api`** (ikke en simulert
respons): søk, anlegg-/banevalg, og import kjørt reelt mot den kjørende
produksjonscontaineren (kun lesing) — importerte Borregaard Golfklubb sin
hovedbane, bekreftet alle 18 hull + 8 tee/tee_rating-rader riktig i
databasen, og opprettet en ekte økt med den importerte banen (beviser hele
veien til handicap-motoren, ikke bare selve importen). Reimport avvist
(409), kryss-org-isolasjon bekreftet, ukjent teeoff-slug ga 404,
`test_isolation.sql` 12/12, ekte typesjekket produksjonsbuild av
frontend-utvidelsen.
**Rullet ut live**, bruker bekreftet eksplisitt: migrasjon 010 mot ekte
`teecup_db`, begge containere redeployet, `teeoff.no` upåvirket.
**Reell bug funnet og fikset samme dag, av en bruker som faktisk testet
funksjonen:** bane-søkeboksen ("Hent bane fra teeoff") var et `<form>`
rendret INNI det ytre økt-opprett-skjemaet — ugyldig, nestet HTML. Å klikke
"Søk" submittet i praksis det ytre skjemaet som en ekte side-navigasjon og
vasket bort `?org=...`-parameteren fra URL-en. Fikset ved å fjerne det
indre `<form>`-elementet (vanlig `<div>` + Enter-tast/knapp-klikk i
stedet). Se CLAUDE.md-status for full root cause.
---
## UX / frontend (senere fase) ## UX / frontend (senere fase)
- 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp - 📋 Høy kontrast, dark/light, store +/- knapper, stor «Neste hull»-knapp

View file

@ -811,8 +811,7 @@ function OfficialCourseSearch({
const [importing, setImporting] = useState(false) const [importing, setImporting] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
async function runSearch(e?: React.FormEvent) { async function runSearch() {
e?.preventDefault()
setSearching(true) setSearching(true)
setError(null) setError(null)
try { try {
@ -901,18 +900,37 @@ function OfficialCourseSearch({
{!selectedFacility ? ( {!selectedFacility ? (
<> <>
<form onSubmit={runSearch} className="flex items-center gap-2"> {/* Bevisst IKKE et eget <form> her -- denne komponenten rendres
inne i CreateSessionCard sitt <form onSubmit={handleSubmit}>,
og nestede <form>-elementer er ugyldig HTML. Nettleseren
slår da sammen de to skjemaene i den faktiske DOM-en, som
gjorde at "Søk"-knappen i praksis submittet det YTRE
økt-skjemaet i stedet -- en ekte native side-navigasjon som
vasket bort ?org=...-parameteren fra URL-en (funnet ved en
reell feilrapport fra bruker mot produksjon). */}
<div className="flex items-center gap-2">
<Input <Input
autoFocus autoFocus
placeholder="Søk anleggsnavn…" placeholder="Søk anleggsnavn…"
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
runSearch()
}
}}
className="h-11 flex-1 rounded-xl text-base" className="h-11 flex-1 rounded-xl text-base"
/> />
<Button type="submit" disabled={searching} className="h-11 shrink-0 rounded-xl font-bold"> <Button
type="button"
onClick={() => runSearch()}
disabled={searching}
className="h-11 shrink-0 rounded-xl font-bold"
>
{searching ? "Søker…" : "Søk"} {searching ? "Søker…" : "Søk"}
</Button> </Button>
</form> </div>
{facilities && ( {facilities && (
<ul className="flex max-h-56 flex-col overflow-auto rounded-xl border border-border"> <ul className="flex max-h-56 flex-col overflow-auto rounded-xl border border-border">
{facilities.map((f) => ( {facilities.map((f) => (