diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 6e07b43..a12281d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -110,7 +110,28 @@ "Bash(python3 -m py_compile /opt/teecup/app/config.py /opt/teecup/app/email.py /opt/teecup/app/routers/auth.py)", "Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -X POST -H 'Content-Type: application/json' -d '{\"email\":\"devlog@test.no\"}' http://127.0.0.1:8099/auth/request-link)", "Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -X POST -H 'Content-Type: application/json' -d '{\"email\":\"hei@erol.no\"}' http://127.0.0.1:8099/auth/request-link)", - "Bash(rm -f __TRACKED_VAR__/002_scratch.sql __TRACKED_VAR__/002_wrapper.sql)" + "Bash(rm -f __TRACKED_VAR__/002_scratch.sql __TRACKED_VAR__/002_wrapper.sql)", + "Bash(docker compose *)", + "Read(//opt/teeoff/deploy/**)", + "Bash(echo \"EXIT: $?\")", + "Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d, indent=2\\)\\)\")", + "Bash(docker restart *)", + "Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -X POST -H 'Content-Type: application/json' -d '{\"email\":\"hei@erol.no\"}' https://teecup.teeoff.no/auth/request-link)", + "Bash(docker stop *)", + "Bash(grep -A2 \"^services:\" -A3 /opt/teecup/docker-compose.yml *)", + "Bash(python3 -c ' *)", + "Bash(docker run *)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" --max-time 10 https://teeoff.no/)", + "Bash(curl -s -w \"\\\\n[HTTP %{http_code}]\\\\n\" --max-time 10 https://teecup.teeoff.no/health)", + "Bash(curl -s -i -c /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/cookies_prod.txt -X POST -H 'Content-Type: application/json' -d '{\"token\":\"oMLyXafK3LDJhXMmVWtS2PaTRe3xfXklXsvR_CGp4m8\"}' https://teecup.teeoff.no/auth/verify-link)", + "Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -b /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/cookies_prod.txt https://teecup.teeoff.no/auth/me)", + "Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/cookies_prod.txt /tmp/health_resp.json *)", + "Bash(git -C /opt/teecup status --short)", + "Bash(git -C /opt/teeoff status --short)" + ], + "additionalDirectories": [ + "/opt/teeoff/deploy", + "/tmp" ] } } diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e49baef --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.env +.git +.gitignore +__pycache__ +*.pyc +.claude diff --git a/CLAUDE.md b/CLAUDE.md index 209d12c..0866387 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -147,15 +147,49 @@ Ferdig og verifisert: **Verifisert med faktisk levering:** sendte én ekte test-e-post til en adresse brukeren oppga — brukeren bekreftet mottak. Første gang noe i prosjektet er bevist ved ekte levering, ikke bare curl/scratch. +- **Containerisert og LIVE på `teecup.teeoff.no` (2026-07-16):** ekte + `teecup_db` opprettet (migrasjoner 001→005 kjørt permanent, `test_isolation.sql` + består), `Dockerfile` + `docker-compose.yml` (tjeneste `teecup_api`, joiner + det eksisterende `teeoff_default`-nettverket), Caddy-blokk lagt til i + `/opt/teeoff/deploy/Caddyfile`. Ekte innlogging (magic-link → e-post → + JWT-sesjon med `Secure`-cookie) verifisert ende-til-ende mot den live + stacken. `teeoff.no` upåvirket gjennom hele prosessen. + **To reelle hendelser underveis, begge løst:** + 1. **Caddy plukket ikke opp filendringen** — `teeoff_caddy` sin + `Caddyfile`-mount er en ENKELTFIL-bind-mount, låst til inoden som fantes + da containeren sist startet. Min fil-redigering (atomisk rename) laget + en ny inode på samme sti, så containeren fortsatte å lese den GAMLE + filen uansett hvor mange ganger `caddy validate`/`caddy reload`/admin-API + `/load` ble kjørt (alle validerte/lastet den uendrede gamle filen, derav + ingen feilmelding). Løst med en full `docker restart teeoff_caddy` + (brukeren bekreftet — avvek fra planens "kun graceful reload, ingen + omstart"-løfte, noen sekunders nedetid for `teeoff.no`). + 2. **Alvorlig nettverksalias-kollisjon** (funnet RETT ETTER omstarten, da + ekte teeoff-trafikk som `/api/facilities?...` med ekte klubb-slugs dukket + opp i `teecup_api` sin logg): `docker-compose.yml` sin service-nøkkel var + `api:` — SAMME nøkkel som teeoffs eget `api`-servicenavn + (`docker-compose.prod.yml`). Docker Compose registrerer nettverksalias + basert på service-NAVNET (ikke bare `container_name`) på delte nettverk, + så BEGGE containerne fikk alias `api` på `teeoff_default` — Caddys + `reverse_proxy api:8000` i teeoff sin egen config kunne da tilfeldig + treffe enten ekte `teeoff_api` eller `teecup_api`. **Rettet umiddelbart** + (stoppet `teecup_api` først for å hindre videre feilruting av ekte + teeoff-trafikk, ga service-nøkkelen navnet `teecup_api` i stedet, + gjenopprettet — bekreftet med `docker network inspect` at alias `api` nå + KUN peker på ekte `teeoff_api`). + **Mindre driftslærdom:** (a) jeg eksponerte ved et uhell + `TEECUP_SMTP_PASS`/`TEECUP_FROM_EMAIL` i eget debug-output mens jeg + feilsøkte en `.env`-korrupsjon (manglende linjeskift fra min egen + `>>`-tilføyelse) — brukeren roterte passordet som forsiktighetsregel; (b) + Docker leser IKKE `.env` på nytt for en allerede kjørende container — + `docker compose up -d --force-recreate` kreves etter enhver `.env`-endring + som skal tas i bruk; (c) et `#`-tegn i et upassordet `.env`-passord kuttes + som en kommentar av Compose sin parser — anførselstegn (fortrinnsvis enkle) + løser dette. Neste steg: -1. Containerisere TeeCup-API-et (Dockerfile + compose-tjeneste), koble mot - `teecup_db` med `teecup_app`, rute via eksisterende Caddy til - `teecup.teeoff.no`. (Under scratch-verifisering måtte hele `/opt/teecup` - monteres, ikke bare `app/`, fordi `handicap_engine.py` er et - toppnivå-søskenmodul til `app`-pakken — Dockerfilen må `COPY` begge inn - med samme relative plassering.) -2. Deretter frontend (PWA, offline-first) og kommunikasjon (migrasjon 006, - siden 004/005 nå er tatt av auth og RLS-fiksen). Frontend er fortsatt IKKE - startet (⬜ i utviklingsplanen i ARCHITECTURE_DECISIONS.md) — API-et alene - er ikke en brukbar nettside. +1. Frontend (PWA, offline-first) og kommunikasjon (migrasjon 006, siden + 004/005 nå er tatt av auth og RLS-fiksen). Frontend er fortsatt IKKE + startet (⬜ i utviklingsplanen i ARCHITECTURE_DECISIONS.md) — helt frem til + nå har API-et vært nåbart, men uten noe grensesnitt en sluttbruker kan + bruke. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b518270 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.12-slim + +WORKDIR /srv + +COPY app/requirements.txt app/requirements.txt +RUN pip install --no-cache-dir -r app/requirements.txt + +COPY app/ app/ +COPY handicap_engine.py handicap_engine.py + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index fae63e9..4edbbd0 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -31,6 +31,62 @@ | 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 `api` på + `teeoff_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). --- diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..432c83d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +services: + teecup_api: + build: . + container_name: teecup_api + environment: + TEECUP_DATABASE_URL: ${TEECUP_DATABASE_URL} + TEECUP_SESSION_SECRET: ${TEECUP_SESSION_SECRET} + TEECUP_SMTP_SERVER: ${TEECUP_SMTP_SERVER} + TEECUP_SMTP_PORT: ${TEECUP_SMTP_PORT} + TEECUP_SMTP_USER: ${TEECUP_SMTP_USER} + TEECUP_SMTP_PASS: ${TEECUP_SMTP_PASS} + TEECUP_FROM_EMAIL: ${TEECUP_FROM_EMAIL} + networks: + - teeoff_default + restart: unless-stopped + +networks: + teeoff_default: + external: true