teecup/CLAUDE.md

261 lines
17 KiB
Markdown
Raw Normal View History

2026-07-16 08:21:57 +02:00
# CLAUDE.md — arbeidsinstruks for TeeCup
Les dette først i hver økt. Det koder hva vi har bestemt og hvordan vi jobber.
## Autoritative kilder (les før du gjør noe)
2026-07-17 21:40:42 +02:00
- `ARCHITECTURE_DECISIONS.md` — hva som er bestemt og hvorfor (ADR-001…015). Fasit.
2026-07-16 08:21:57 +02:00
- `FEATURE_BACKLOG.md` — hva som gjenstår, hva som er utsatt, hva som mangler.
- Endres en beslutning: legg til en ny ADR, ikke slett historikk. Hold begge
filene oppdatert når noe avgjøres.
## Sikkerhetsregler (ufravikelige)
- Rør ALDRI `teeoff`-databasen eller den ekte `teecup_db` uten at brukeren
eksplisitt har bekreftet det i samme økt. Test alltid migrasjoner mot en egen
scratch-database først, og rydd opp etterpå.
- Vis planen (hvilke kommandoer, mot hvilken database) FØR du kjører noe som
skriver, migrerer eller sletter. Vent på bekreftelse.
- Hemmeligheter (passord, secrets) bor i `.env` (filrettigheter 600), dekkes av
`.gitignore`, committes aldri, og skrives aldri i klartekst i chatten eller i
SQL-filer. Generer dem på serveren (`openssl rand -base64 32`).
- Kjør appen som databaserollen `teecup_app` (NOSUPERUSER, NOBYPASSRLS) — aldri
som `teeoff_admin`/superuser i runtime.
## Arkitektur-invarianter (ikke bryt uten en ny ADR)
- Tenant = organisasjon. `organization_id` på alle domenetabeller, håndhevet av
RLS. App-koden setter `app.current_org` med `SET LOCAL` per transaksjon.
- Verifiser at brukeren er medlem av organisasjonen FØR org-konteksten settes.
RLS stoler blindt på `app.current_org`.
- Egen innlogging (uavhengig av teeoff). Banedata hentes fra teeoff via lesende
API, ikke delt database.
- v1 = nøyaktig to lag (Ryder Cup-format), håndhevet i app-laget. Match-modellen
holdes generell (to sider) så knockout/flere lag kan komme senere.
- Handicap-/matchlogikk skal ligge i `handicap_engine.py` (rent, testet, uten
db/API-avhengigheter). Allowances er konfig, ikke hardkodet.
- Media (bilder/video) skal i objektlagring (MinIO), ikke i Postgres. Postgres
holder bare metadata + nøkkel.
## Arbeidsmåte
- Inkrementelt. Ingenting tas for gitt før det er testet. Bekreft hvert steg før
du går videre.
- Bruk git (remote: brukerens Forgejo). Commit i logiske steg med tydelige
meldinger.
- Er du usikker på omfang eller en beslutning: spør heller enn å gjette.
## Status (oppdater denne når ting endres)
Ferdig og verifisert:
- Handicap-motor + tester (24/24, R&A-verifisert).
- Skjema `001` + roller `002` + scoring/blind draw `003`. Isolasjon bevist med
`test_isolation.sql` (RLS-oppførsel, ikke bare at skjemaet kjører).
- 002 hadde en reell bug (psql interpolerer ikke `:'var'` inne i `DO $$...$$`)
— permanent fikset, verifisert mot scratch to ganger.
- API-et kjørt for ekte (ikke bare syntaks-sjekket) i en engangs Docker-
container mot en scratch-database, RLS bevist gjennom hele
asyncpg-pool-stacken (ikke bare i rå SQL).
- Oppsett-endepunktene er bygget og verifisert: `app/routers/players.py`
(spillerpool), `tournaments.py` (turnering/lag/roster/økter, ADR-011
to-lags-grense håndhevet med `FOR UPDATE`-lås), `matches.py` (matcher/
deltakere/blind draw-lås, ADR-013-synlighet push-down i SQL). Delt
feiloversettelse i `app/errors.py`, delte synlighetsspørringer i
`app/blind_draw.py`. `main.py` er nå bare app-factory + `include_router`.
- Scoring-runden er bygget og verifisert for ekte mot scratch-db (18-hulls
bane med `tee_rating`, 4 spillere for fourball-testing): `app/handicap.py`
(ADR-014 fire brytere via `parse_allowance_config`, handicap beregnes i
`compute_and_store_side_handicaps` rett etter deltaker-innsetting —
singles/fourball per spiller umiddelbart, foursome/greensome/scramble kun
når siden er komplett), `app/routers/scoring.py` (`hole-scores`/
`hole-results`-upsert, `scorecard`-GET, matchstatus-recompute med
`FOR UPDATE`-lås mot race og SAMMENHENGENDE-prefiks-regel for uferdige
hull). `app/team_authz.py` skilt ut fra `matches.py` (delt med
`scoring.py`). Alle 10 planlagte tester bestått, inkl. fourball
better-ball-aggregering (MIN av to nettoer, venter til begge partnere har
registrert), poeng-caching ved tidlig avgjort match, og ADR-014-bryteren
`use_handicap=false`.
**Fant og fikset underveis:** `tournaments.py` sin `SessionCreate` manglet
`scoring_mode` helt (økter kunne aldri opprettes i `hole_result`-modus via
API-et) — lagt til.
**Bevisst utelatt/kjente begrensninger:** en side som aldri når forventet
deltakerantall (no-show) får aldri beregnet handicap og matchen kan da
aldri avgjøres — ingen manuell overstyring bygget. Score-skriving er
upsert (ingen avvisning ved duplikat) — ingen audit-trail på rettelser.
Kapteins-only autorisasjon fortsatt ikke bygget (FEATURE_BACKLOG ❓); bar
er «rostret på laget».
Ekte autentisering er bygget og verifisert. X-Debug-User-Id-stubben er helt fjernet, ingen fallback beholdt. Ny flyt: magic-link (POST /auth/request-link → POST /auth/verify-link) + JWT-sesjon i HttpOnly/SameSite=Lax/dynamisk-Secure-cookie (30 dager), pluss /auth/logout og /auth/me. Ny migrasjon 004_auth.sql (unik e-post-indeks + magic_link_token-tabell). Sikkerhetsdesignet fra Plan-agent-gjennomgangen holdt gjennom testing: Token: secrets.token_urlsafe(32), kun SHA-256-hash lagres Atomisk forbruk (UPDATE...RETURNING, ikke les-sjekk-skriv) — hindrer replay Generisk respons uansett om e-posten finnes — hindrer enumerering app_user opprettes først ved vellykket verifisering, ikke ved forespørsel — hindrer massopprettelse Gamle uforbrukte lenker ugyldiggjøres når en ny utstedes PyJWT (byttet fra python-jose pga. bredere sårbarhetsflate) med eksplisitt algorithms=["HS256"] Ekte eksistens-sjekk mot app_user på hvert kall — en slettet bruker mister tilgang umiddelbart, ikke etter 30 dager Alle 12 planlagte tester bestått, inkludert cooldown, token-ugyldiggjøring, utløp, tuklet JWT, slettet bruker, og at debug-headeren nå er helt uten effekt. To ting funnet og fikset/dokumentert underveis: ON CONFLICT (email) matchet ikke den nye partielle unike indeksen uten eksplisitt WHERE-klausul — fikset. En reell, dypere RLS-bug (dokumentert i FEATURE_BACKLOG.md, ikke fikset her): organization-tabellens RLS-policy kaster en 500 i stedet for "se ingenting" når app.current_org leses tilbake som tomstreng (ikke NULL) på en gjenbrukt pool-tilkobling. Berører trolig alle 15 RLS-policyer i skjemaet — for stort og sensitivt (ADR-003-grunnmuren) til å hastefikse her, så jeg mitigerte det lokalt i /auth/me og satte det som punkt 1 i neste-steg-listen.
2026-07-16 15:16:53 +02:00
- **Match-lås ved avgjørelse (2026-07-16):** `submit_hole_score`/
`submit_hole_result` avviser nå 409 hvis `match.points_side_a IS NOT NULL`
(matchen er avgjort) — FØR upserten kjøres, både for nye hull og
korrigering av allerede talte hull. Tetter en reell bug: uten dette kunne
«spøkelses-hull» lagt inn etter avgjørelse endre en allerede cachet margin
ved neste omregning. Automatisk, ingen ny autorisasjon involvert.
- **Ekte autentisering bygget og verifisert (2026-07-16):** `X-Debug-User-Id`-
stubben er HELT fjernet (ingen fallback). Magic-link + JWT-sesjon i
`app/routers/auth.py` + `app/auth.py` (`request-link`/`verify-link`/
`logout`/`me`), ny migrasjon `004_auth.sql` (`magic_link_token`-tabell +
unik e-post-indeks på `app_user`). Token = `secrets.token_urlsafe(32)`, kun
SHA-256-hash lagres, atomisk forbruk (`UPDATE ... RETURNING`, ikke
les-sjekk-skriv), generisk respons uansett om e-posten finnes (unngår
enumerering), gamle uforbrukte lenker ugyldiggjøres når en ny utstedes,
`app_user` opprettes FØRST ved vellykket verifisering (ikke ved
forespørsel). Sesjons-JWT (PyJWT, `algorithms=["HS256"]` eksplisitt) i
HttpOnly/SameSite=Lax/dynamisk-Secure-cookie, 30 dager, med et ekte
eksistens-oppslag mot `app_user` på hver forespørsel (faktisk
tilbakekalling — en slettet bruker kan ikke ri ut sesjonen). Alle 12
planlagte tester bestått.
**Fant og fikset underveis:** `ON CONFLICT (email)` matchet ikke den nye
PARTIELLE unike indeksen uten eksplisitt `WHERE email IS NOT NULL` (samme
klasse feil som `hole_score`s partielle indekser i scoring-runden).
RLS-tomstreng-buggen er fikset og verifisert grundig. Fiksen: ny migrasjon 005_rls_null_guard.sql — én delt STABLE SQL-funksjon app_current_org() (NULLIF(current_setting('app.current_org', true), '')::uuid) erstatter det rå uttrykket i alle 15 RLS-policyer (14 org_isolation + org_self) via ALTER POLICY. NULLIF konverterer tomstreng til NULL før cast, så "trygg standard: se ingenting" gjenopprettes uansett GUC-tilstand. Verifisert to ganger, ulikt: Tre nye regresjonstester i test_isolation.sql (Test 10–12): tomstreng-lesing gir 0 rader ikke krasj, tomstreng-skriving avvises av RLS ikke krasj, og org-bootstrap-innsetting (tidligere aldri testet) fungerer rett etter tomstreng-tilstand. Faktisk gjenskaping av original-buggen mot en ekte container (pool-størrelse tvunget til 1 for å garantere tilkoblings-gjenbruk): varmet opp med et vanlig org_connection()-kall, kalte deretter /auth/me på samme tilkobling — gikk fra 500 til 200. En viktig ting jeg tok feil om i forrige runde, oppdaget ved å faktisk teste i stedet for å anta: fiksen gjør ikke at /auth/me trygt kan joine organization direkte via plain_connection(). Den løser tomstreng-krasjen, men org_self-policyen krever fortsatt en matchende app.current_org for å vise noen rad i det hele tatt — riktig RLS-design, ikke noe fiksen skulle endre. Siden en bruker kan tilhøre flere organisasjoner, finnes det ingen én kontekst å sette. Rettet til riktig løsning: /auth/me slår nå opp hvert org-navn enkeltvis via org_connection() (verifisert at det faktisk returnerer navnet korrekt). Alt dokumentert i CLAUDE.md/FEATURE_BACKLOG.md.
2026-07-16 15:34:24 +02:00
**Fant, IKKE fikset i denne runden (egen runde rett etterpå — se under):**
`organization`-tabellens RLS-policy kastet en 500 i stedet for skjemaets
lovede "trygg standard: se ingenting" ved tomstreng-GUC.
- **RLS-tomstreng-bug FIKSET (2026-07-16):** ny migrasjon
`005_rls_null_guard.sql` — delt `STABLE` SQL-funksjon `app_current_org()`
gjør `NULLIF(current_setting('app.current_org', true), '')::uuid` i stedet
for det rå uttrykket, brukt av alle 15 RLS-policyer (`ALTER POLICY`,
14 `org_isolation` + `org_self`). Verifisert med 3 nye regresjonstester i
`test_isolation.sql` (Test 10-12) OG ved faktisk å gjenskape original-
buggen mot en ekte container (pool-størrelse 1, varm opp med
`org_connection()`, deretter `/auth/me` på samme gjenbrukte tilkobling —
gikk fra 500 til 200).
**Viktig presisering fra denne runden:** fiksen gjør IKKE at `/auth/me` kan
joine `organization` direkte via `plain_connection()` — det var en feilaktig
antakelse i forrige runde. `org_self` krever fortsatt en MATCHENDE
`app.current_org` for å vise en rad (riktig RLS-design, ikke noe fiksen
skulle endre), og en bruker kan tilhøre flere organisasjoner samtidig, så
det finnes ingen ÉN kontekst å sette for en tverr-org-spørring. `/auth/me`
slår derfor opp hvert org-navn ett om gangen via `org_connection()` (N+1,
N = antall org-er brukeren tilhører) — dette er riktig løsning, ikke en
omvei.
- **Organisasjon-bootstrap bygget og verifisert (2026-07-16):** nytt
`POST /orgs` (`app/routers/organizations.py`) — det ENESTE stedet i API-et
som setter inn en `organization`-rad. Fant under statusgjennomgang at dette
manglet helt (alle tidligere org-er var seedet med superbruker-SQL). Ingen
ny migrasjon. Selvrefererende RLS-bootstrap bekreftet å fungere: generer
org-ens uuid i Python, sett `app.current_org` til nøyaktig den via
eksisterende `org_connection()`, sett inn `organization`-raden med samme
id — `org_self`s implisitte `WITH CHECK` blir da trivielt sann, ingen
privilegert tilkobling nødvendig (i motsetning til hva 002s kommentar
antydet). Verifisert med 5 tester inkl. en negativ kontroll (mismatchende
id avvist med `insufficient_privilege`) og full kryss-org-isolasjon mellom
to uavhengig opprettede organisasjoner.
- **Ekte SMTP-utsending bygget og verifisert (2026-07-16):** ny `app/email.py`
(`send_magic_link_email`, `smtplib` via `asyncio.to_thread`, håndterer
både implisitt TLS/port 465 og STARTTLS dynamisk). Brukeren la egne
SMTP-credentials i `.env` (`TEECUP_SMTP_*`, `TEECUP_FROM_EMAIL` — ADR-009,
ikke delt med teeoff); jeg leste kun nøkkelnavnene for å bekrefte de
fantes, aldri verdiene. `app/config.py` sin `SMTP_CONFIGURED` er valgfri
(ikke `_required`) — dev-only logging (`TEECUP_DEV_LOG_MAGIC_LINKS`)
fortsatt fungerer uendret når SMTP ikke er satt opp. Driftsfeil i
utsendingen lekker aldri til klientresponsen (bevarer anti-enumerering).
**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.
TeeCup er nå containerisert og live på https://teecup.teeoff.no. Dette var den mest hendelsesrike runden denne økten — første gang noe rørte ekte, permanent infrastruktur, og det viste seg berettiget: To reelle driftshendelser, begge funnet og rettet i sanntid: Caddy plukket ikke opp filendringen min — enkeltfil-bind-mount er låst til inoden fra da containeren sist startet; min redigering (atomisk rename) laget en ny inode, så validate/reload/admin-API opererte alle stille på den gamle filen. Løst med full omstart av teeoff_caddy (du bekreftet eksplisitt, siden det avvek fra planens "ingen omstart"-løfte). Alvorlig nettverkskollisjon — oppdaget rett etterpå da ekte teeoff-trafikk (/api/facilities?... med ekte klubbnavn) dukket opp i teecup_api sin logg. docker-compose.yml sin service-nøkkel api: kolliderte med teeoffs eget api-servicenavn på det delte nettverket — Docker Compose gir service-navn som DNS-alias, så begge containerne delte alias api, og Caddy kunne tilfeldig sende ekte brukertrafikk til TeeCup i stedet. Stoppet teecup_api umiddelbart, ga den navnet teecup_api i stedet, bekreftet kollisjonen er borte. Mindre ting underveis: jeg eksponerte ved et uhell to secret-verdier i eget debug-output (du roterte passordet), og lærte at Docker ikke leser .env på nytt for en kjørende container (krever --force-recreate etter hver endring) og at # i et upassordet passord kuttes som kommentar av Compose. Sluttresultat, verifisert ende-til-ende mot den ekte, live stacken: teeoff.no upåvirket gjennom hele prosessen, teecup.teeoff.no/health → 200 med automatisk TLS, full magic-link-innlogging med ekte e-postlevering og Secure-cookie. CLAUDE.md/FEATURE_BACKLOG.md oppdatert med full detaljer, inkludert en generell lærdom for fremtidige tjenester på delt nettverk. To repoer har uncommittede endringer: /opt/teecup (nye Dockerfile/docker-compose.yml/.dockerignore + statusfiler) og /opt/teeoff (Caddyfile-endringen, egen repo). Ingenting committet ennå — si ifra når du vil det.
2026-07-16 21:47:34 +02:00
- **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``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`
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.
2026-07-17 21:40:42 +02:00
- **Dato/klokkeslett + feilkode-kontrakt + i18n-forberedelse (2026-07-17,
ADR-015):** reist av brukeren rett før frontend-arbeidet. Ny migrasjon
`006_scheduling_and_locale.sql` (alt additivt): `tournament.end_date`,
`session.scheduled_at`/`tee_interval_minutes`/`start_hole`,
`match.tee_time_override`, `app_user.preferred_locale`,
`magic_link_token.locale`. `match.tee_time` er UTLEDET i Python
(`scheduled_at + (sequence-1)*tee_interval_minutes`, override vinner hvis
satt) — aldri lagret per match. Full retrofit av ALLE 39 daværende
`HTTPException(..., detail="norsk streng")`-steder på tvers av 6 filer til
en delt `app_error(status_code, code, message)`-factory
(`app/errors.py`) — responsformen er nå konsekvent
`{"detail":{"code":...,"message":...}}` i hele API-et, verifisert med et
siste `grep -rn 'detail="' app/` som ga NULL treff. i18n: `locale`
(`nb`/`en`) sendes av klienten ved `request-link`, styrer e-postmalen
(ekte engelsk mal lagt inn i `app/email.py`, ikke bare rørlegging) OG
settes som en HELT NY brukers `preferred_locale` — en eksisterende bruker
som logger inn på et annet språk får IKKE sin lagrede preferanse
overskrevet.
**Fant og fikset underveis:** `tournament.start_date` har ligget i
skjemaet siden migrasjon 001, men var ALDRI koblet til
`TournamentCreate`/`Tournament`-modellene — funnet som en naturlig
bivirkning av å legge til `end_date`.
**Verifisert grundig mot fersk `teecup_scratch`** (001→006,
`test_isolation.sql` fortsatt 12/12): feilkode-form bekreftet på tvers av
5 filer (`NOT_FOUND`/`LIMIT_REACHED` tournaments.py, `DUPLICATE` roster,
`NOT_AUTHENTICATED` uten cookie, `NOT_ROSTERED_ON_TEAM` matches.py),
tee_time-beregning bekreftet (10 min intervall → 08:00/08:10/08:20,
override vinner, `scheduled_at=null` gir `tee_time:null` ikke feil), full
i18n-runde bekreftet (ny bruker `locale:"en"``preferred_locale:"en"`;
påfølgende `request-link` for samme bruker med `locale:"nb"` skiftet
e-postmalen men IKKE den lagrede preferansen, bekreftet via `/auth/me`).
**Kjørt mot ekte `teecup_db` 2026-07-17** (bruker bekreftet eksplisitt i
samme økt): migrasjonen kjørte rent, `test_isolation.sql` fortsatt 12/12
(ruller alltid tilbake, ingen domenerader berørt).
**Mindre driftslærdom, funnet OG rettet samme runde:** et `.env`-filter
(`grep -v -i 'pass|secret|key'`) jeg brukte for å lese ikke-sensitive
nøkler fanget ikke opp `TEECUP_DATABASE_URL`, som bar `teecup_app`-
passordet innebygd i selve URL-en (i tillegg til at det SAMME passordet
allerede lå rent i `TEECUP_APP_PASSWORD` — duplisert, ikke bare skjult) —
passordet ble dermed synlig i et verktøyresultat. Flagget til brukeren
umiddelbart (samme mønster som SMTP-passord-hendelsen over).
- **`.env`/tilkobling ryddet opp (2026-07-17), samme runde:** roten til
hendelsen over var at `TEECUP_DATABASE_URL` var én sammensatt DSN-streng
med passordet URL-kodet inni — usynlig for navnebaserte secret-filtre.
Erstattet med fem separate, rent navngitte felt:
`TEECUP_DB_HOST`/`TEECUP_DB_PORT`/`TEECUP_DB_NAME`/`TEECUP_DB_USER`/
`TEECUP_DB_PASS` (sistnevnte omdøpt fra `TEECUP_APP_PASSWORD`, kun
nøkkelnavnet — verdien aldri lest eller skrevet av meg). `app/config.py`
og `app/db.py` bygger nå `asyncpg`-poolen fra disse fem separate feltene
(`host=`/`port=`/`user=`/`password=`/`database=`) i stedet for én DSN —
fjerner også URL-prosentkoding-problemet fra SMTP-passord-hendelsen sin
klasse av feil. `docker-compose.yml` sin `environment:`-liste oppdatert
tilsvarende. **Krevde et fullt image-rebuild, ikke bare
`--force-recreate`:** `Dockerfile` sin `COPY app/ app/` bakes inn i
imaget ved build-tid (ingen bind-mount i prod, i motsetning til
scratch-verifiseringens engangscontainere) — en ren `--force-recreate`
gjenbrukte det GAMLE imaget og krasjet umiddelbart på den nå fjernede
`TEECUP_DATABASE_URL`. Rettet med `docker compose up -d --build
--force-recreate`. **Verifisert ende-til-ende mot den live stacken:**
containeren boot-et rent (`Application startup complete` i loggen, som
krever en vellykket `init_pool()` — asyncpg ville kastet og forhindret
akkurat den logglinjen ved feil tilkoblingsparametre), `/auth/me` over
ekte https ga et rent `401 NOT_AUTHENTICATED` (ikke 500/502), `teeoff.no`
upåvirket (`200` gjennom hele omstarten, kun `teecup_api` restartet — ikke
delt Caddy-instans, ikke samme risikoklasse som Caddy-hendelsen fra
containeriseringsrunden).
2026-07-16 08:21:57 +02:00
Neste steg:
2026-07-17 21:40:42 +02:00
1. Frontend (PWA, offline-first) og kommunikasjon. Frontend er fortsatt IKKE
TeeCup er nå containerisert og live på https://teecup.teeoff.no. Dette var den mest hendelsesrike runden denne økten — første gang noe rørte ekte, permanent infrastruktur, og det viste seg berettiget: To reelle driftshendelser, begge funnet og rettet i sanntid: Caddy plukket ikke opp filendringen min — enkeltfil-bind-mount er låst til inoden fra da containeren sist startet; min redigering (atomisk rename) laget en ny inode, så validate/reload/admin-API opererte alle stille på den gamle filen. Løst med full omstart av teeoff_caddy (du bekreftet eksplisitt, siden det avvek fra planens "ingen omstart"-løfte). Alvorlig nettverkskollisjon — oppdaget rett etterpå da ekte teeoff-trafikk (/api/facilities?... med ekte klubbnavn) dukket opp i teecup_api sin logg. docker-compose.yml sin service-nøkkel api: kolliderte med teeoffs eget api-servicenavn på det delte nettverket — Docker Compose gir service-navn som DNS-alias, så begge containerne delte alias api, og Caddy kunne tilfeldig sende ekte brukertrafikk til TeeCup i stedet. Stoppet teecup_api umiddelbart, ga den navnet teecup_api i stedet, bekreftet kollisjonen er borte. Mindre ting underveis: jeg eksponerte ved et uhell to secret-verdier i eget debug-output (du roterte passordet), og lærte at Docker ikke leser .env på nytt for en kjørende container (krever --force-recreate etter hver endring) og at # i et upassordet passord kuttes som kommentar av Compose. Sluttresultat, verifisert ende-til-ende mot den ekte, live stacken: teeoff.no upåvirket gjennom hele prosessen, teecup.teeoff.no/health → 200 med automatisk TLS, full magic-link-innlogging med ekte e-postlevering og Secure-cookie. CLAUDE.md/FEATURE_BACKLOG.md oppdatert med full detaljer, inkludert en generell lærdom for fremtidige tjenester på delt nettverk. To repoer har uncommittede endringer: /opt/teecup (nye Dockerfile/docker-compose.yml/.dockerignore + statusfiler) og /opt/teeoff (Caddyfile-endringen, egen repo). Ingenting committet ennå — si ifra når du vil det.
2026-07-16 21:47:34 +02:00
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.