diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 243b935..3330b47 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -286,7 +286,13 @@ "Bash(curl -s -o /dev/null -w \"teeoff.no: %{http_code}\\\\n\" https://teeoff.no)", "Bash(python3 -c \"import ast; ast.parse\\(open\\('app/routers/registration.py'\\).read\\(\\)\\)\")", "Bash(mkdir -p /opt/teecup/frontend/app/tournaments/\\\\[id\\\\]/teams/\\\\[teamId\\\\]/chat)", - "Bash(grep -n -A 20 \"teecup.teeoff.no\" /opt/teeoff/deploy/Caddyfile)" + "Bash(grep -n -A 20 \"teecup.teeoff.no\" /opt/teeoff/deploy/Caddyfile)", + "Bash(curl -s -o /dev/null -w \"teecup health: %{http_code}\\\\n\" https://teecup.teeoff.no/health)", + "Bash(curl -s -o /dev/null -w \"teecup dashboard: %{http_code}\\\\n\" https://teecup.teeoff.no/dashboard)", + "Bash(curl -s -o /dev/null -w \"ws-route \\(uten upgrade-header\\): %{http_code}\\\\n\" https://teecup.teeoff.no/ws/orgs/test/teams/test/messages)", + "Bash(curl -s https://teecup.teeoff.no/ws/orgs/test/teams/test/messages)", + "Bash(curl -s -H \"Content-Type: application/json\" https://teecup.teeoff.no/health)", + "Bash(docker run --rm python:3.12-slim bash -c 'pip install -q websockets 2>/dev/null; python3 -c \" *)" ], "additionalDirectories": [ "/opt/teeoff/deploy", diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index 16436eb..f07ec65 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -1093,7 +1093,7 @@ feed-innlegg. Lag-chat har INGEN moderering utover forfatteren selv (rommet er privat, org-admin har uansett ikke lesetilgang og kan derfor ikke moderere det). -**Status: ✅ BYGGET 2026-07-19.** Se CLAUDE.md-status for scratch- +**Status: ✅ BYGGET OG LIVE 2026-07-19.** Se CLAUDE.md-status for scratch- verifisering og utrulling, inkl. en egen Caddy-rute (`/ws/*`) for WebSocket-trafikk direkte til `teecup_api` (Next.js sin `rewrites()` proxyer ikke WebSocket-oppgraderinger pålitelig — samme klasse diff --git a/CLAUDE.md b/CLAUDE.md index aeeef51..dc8c13a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1480,14 +1480,118 @@ Ferdig og verifisert: begge containere boot-et rent, `/health` og `/dashboard` → 200, `teeoff.no` upåvirket. +- **Kommunikasjon LIVE (2026-07-19, ADR-025) — det største enkeltløftet i + prosjektet så langt:** lag-intern chat («det hemmelige rommet») + offentlig + runde-feed («Banter Board»), begge med bilder og ekte WebSocket-sanntid, + bygget i samme runde. Fire hovedbeslutninger avklart eksplisitt med bruker + (AskUserQuestion) før bygging — brukeren valgte den mest ambisiøse + kombinasjonen på alle fire (begge deler nå, WebSockets fremfor polling, + ekte privat chat, bilder fra start). + **Datamodell:** ny migrasjon `013_messaging.sql` — delt `message`-tabell + med `scope`-diskriminator (`team`/`tournament_feed`) i stedet for to + separate tabeller, RLS org_isolation som ellers. `author_display_name` + FRYSES ved skrivetidspunkt (samme prinsipp som handicap-snapshot, + ADR-007) — spillerens `player.display_name` i org-en hvis den finnes, + ellers e-postens lokaldel (dekker org-ansatte uten egen spillerprofil). + **Lag-chat er BEVISST ekte privat** — ny `user_is_rostered_on_team` i + `app/team_authz.py`, med VILJE uten org-admin-fallback, ulikt de to andre + funksjonene i samme fil (`user_is_team_captain`/`user_is_match_participant`, + ADR-023, som begge har et slikt unntak). Første sted i hele appen der + org-eier/admin er strukturelt utestengt fra noe. + **Offentlig feed:** LESING gjenbruker `registration.py` sitt eksisterende + trenivå-visibility-mønster (ADR-018) helt uendret, inkl. anonym tilgang. + POSTING er strengere enn lesing — krever ekte innlogging OG org- + medlemskap/faktisk deltakelse, selv på en `public`-synlig turnering (en + helt urelatert innlogget bruker skal ikke kunne poste på en fremmed + offentlig side). Moderering: forfatteren selv ELLER org-eier/admin kan + slette et feed-innlegg (motsatt av lag-chatten, som ikke har noen + ekstern moderator). + **`registration.py` sine fire interne hjelpefunksjoner gjort delt** + (fjernet ledende understrek — samme "gjort delt for gjenbruk"-mønster som + tidligere runder): `resolve_org`, `is_participant`, `code_matches`, + `check_visibility`. `team_authz.py` sin `_is_org_admin` likeens → + `is_org_admin`. Ingen atferdsendring, kun navn, for at `messaging.py` + skulle kunne gjenbruke dem uendret i stedet for å duplisere logikk. + **WebSockets, ikke polling:** in-memory tilkoblingsregister PER PROSESS i + `app/routers/messaging.py` — trygt med dagens ene `teecup_api`-container, + men deles IKKE på tvers av flere prosesser/containere (samme klasse + begrensning som den allerede aksepterte in-memory-cachen, se ARCHITECTURE_ + DECISIONS.md "Åpne spørsmål"). Ny `get_current_user_from_websocket` i + `app/auth.py` — WS-ruter kan ikke bruke `get_current_user`/ + `get_authorized_org` direkte via `Depends()` (de er `Request`-typet, ingen + ekte HTTP Request finnes i en WS-scope), derfor en bevisst minimal, egen + kopi av samme cookie-dekode-/oppslagslogikk. + **Reell infrastrukturoppdagelse FØR noe ble forsøkt, ikke i etterkant:** + Next.js sin `rewrites()` proxyer ikke WebSocket-oppgraderinger pålitelig i + "standalone"-modus — løst likt som MinIO-media-ruten (ADR-018): en egen + Caddy-rute (`handle /ws/* { reverse_proxy teecup_api:8000 }`) rett til + API-et, forbi Next.js/`teecup_frontend` helt. Caddyfile ligger i det + SEPARATE `/opt/teeoff`-repoet — samme stale-bind-mount-inode-oppførsel som + ALLE tidligere Caddyfile-runder (graceful reload plukker ikke opp + endringen), løst likt: full `docker restart teeoff_caddy`, brukeren + bekreftet eksplisitt på forhånd, noen sekunders nedetid for `teeoff.no`. + **Verifisert grundig mot fersk `teecup_scratch`** (isolert + `teecup_app_scratch`-rolle, isolert scratch-MinIO, engangs API-container + med `websockets`-Python-biblioteket installert kun for testen): et + 20-punkts asyncio/httpx/websockets-testskript som dekket BEGGE + meldingstyper ende-til-ende. Kritiske personvern-/sanntid-funn, alle + bekreftet med ekte tilkoblinger (ikke bare REST): + - Rostret spiller på lag A FÅR lese/skrive lag A sin chat; rostret spiller + på lag B NEKTES; **organisatoren (org-eier) NEKTES OGSÅ** — bekreftet + BÅDE over REST (`GET`) og over selve WebSocket-håndtrykket (avvist med + lukkekode 4403 før `accept()` i det hele tatt kalles). + - Sanntid bekreftet reelt: A1 koblet til lag A sin chat-socket, A2 sendte + en melding over vanlig REST, A1 mottok den umiddelbart over den åpne + WebSocket-tilkoblingen (ikke bare at REST-svaret så riktig ut). + - Bildeopplasting i chat bekreftet (ekte AVIF-konvertert `image_url` + returnert). + - Offentlig feed: anonym NEKTES å poste (401), en tilfeldig INNLOGGET men + uvedkommende bruker NEKTES (403 `NOT_A_PARTICIPANT`), org-medlem FÅR, + en faktisk deltaker (rostret, IKKE org-medlem) FÅR — beviser + `is_participant`-veien fungerer uavhengig av `is_member`-veien. Anonym + WebSocket-tilkobling til en `public`-synlig turnerings feed FÅR lov og + mottar sanntidsoppdateringer. + - Moderering bekreftet: forfatter sletter eget innlegg, org-admin sletter + ANDRES innlegg (feeden), uvedkommende NEKTES å slette andres innlegg. + `test_isolation.sql` 12/12 uendret (additiv migrasjon). Ekte typesjekket + produksjonsbuild av frontend kjørt og bekreftet, inkl. den nye + `/tournaments/[id]/teams/[teamId]/chat`-ruten. + **Frontend:** ny `components/team-chat.tsx` (meldingsliste med egen/andres- + styling, bildeopplasting, sanntid via nettleserens native `WebSocket`, + slett-egen-melding), lenket fra en ny chat-ikon-knapp i `tournament- + detail.tsx` sin `TeamPanel`. Ny seksjon `TournamentFeed` i + `components/public-tournament.tsx` (nederst på den offentlige + turneringssiden) — viser 401/403-svar fra posting som forklarende + inline-tekst ("logg inn for å poste" / "du må være medlem/deltaker") i + stedet for en generisk feilmelding. + **Rullet ut live 2026-07-19**, bruker bekreftet eksplisitt for alle tre + stegene (migrasjon, containere, Caddy-restart): migrasjon 013 kjørt mot + ekte `teecup_db`, `test_isolation.sql` fortsatt 12/12, begge containere + boot-et rent, Caddy validert (`caddy validate` — "Valid configuration") + FØR restart, restarten ren (ingen feil i loggen). Verifisert grundig + etterpå: `/health`/`dashboard` → 200, `teeoff.no` → 200, et ekte + `wss://`-håndtrykk over produksjons-https bekreftet å nå helt frem til + applikasjonslaget (testet mot en ukjent turnering-id — ingen ekte data + berørt — ga korrekt 404 fra selve WS-ruten, ikke en Caddy/Next.js-feil), + og et ren-HTTP-kall mot en `/ws/*`-sti bekreftet å returnere FastAPI sin + egen JSON-404 (`{"detail":"Not Found"}`) og ikke Next.js sin HTML-404 — + beviser Caddy-ruten faktisk treffer `teecup_api`, ikke `teecup_frontend`. + 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: sanntid/WebSockets vs. - polling, tilskuer-rolle (bevisst utsatt til Kommunikasjon-runden), - kode-regenerering for ADR-020, turnering-status (draft/active/completed/ - archived) via API, korrigering-godkjenning fra motpart. -4. Ved fremtidige nye V0-skjermer/-design i V0 (fortsett i samme prosjekt), +2. Fortsatt åpne beslutninger fra FEATURE_BACKLOG.md: tilskuer-rolle (bevisst + utsatt til nå at Kommunikasjon er ferdig — bør tas fatt på snart, samme + begrunnelse som før: definer sammen med feed-synligheten, som nå finnes), + kode-regenerering for ADR-020, korrigering-godkjenning fra motpart, live + leaderboard (samme WebSocket-mekanisme som meldinger kan gjenbrukes, ikke + koblet til ennå), video/1-til-1-meldinger (bevisst utsatt i ADR-025). +3. Flere turneringsformater utover Ryder Cup (Københavner/High-low-high/ + Robbins/Try all, notert 2026-07-19) — ingen ADR-runde startet ennå. +4. **Merk for neste økt:** `deploy/Caddyfile`-endringen (ny `/ws/*`-rute) + ligger uncommitted i det SEPARATE `/opt/teeoff`-repoet, ikke i + `teecup`-repoet — samme fallgruve som ADR-016-runden sin Caddy-endring, + lett å glemme siden denne økten ellers kun har jobbet i `/opt/teecup`. +5. 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 diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index 04a0fb8..1558a0a 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -464,10 +464,12 @@ - «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- - backend-endepunktet (se egen seksjon over), men ikke sanntidsleveringen - eller frontend-skjermen ennå. Valg: WebSockets vs. polling. +- **Status:** ✅ mekanisme LIVE 2026-07-19 (ADR-025), men KUN koblet til + meldinger ennå. WebSockets valgt (ikke polling) — se Kommunikasjon- + seksjonen. `app/routers/messaging.py` sitt in-memory tilkoblingsregister + og broadcast-mønster kan gjenbrukes for et live leaderboard senere (samme + backend-endepunkt finnes allerede), men det er IKKE gjort i denne runden + — egen, liten oppfølgingssak om/når etterspurt. ### Knockout / cup-turnering (egen turneringstype) - **Status:** 💤 utsatt (egen fremtidig type, ADR-011) @@ -537,16 +539,22 @@ --- -## Kommunikasjon (under design) +## Kommunikasjon — ✅ HELT FERDIG 2026-07-19 (ADR-025) | Del | Status | Notat | |---|---|---| -| Lag-intern chat («det hemmelige rommet») | 🔨 | Bekreftet ønsket. Kanal m/ scope `team`. | -| Offentlig runde-feed («Banter Board») | ❓ | Synlighetsnivå fortsatt ikke besluttet for FEED-en spesifikt, men mekanismen finnes nå: `tournament.visibility` + `get_current_user_optional`/`_is_participant()` (ADR-018) er bygget og live — gjenbruk dette, ikke bygg en ny mekanisme. | -| 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. | +| Lag-intern chat («det hemmelige rommet») | ✅ LIVE | `/tournaments/[id]/teams/[teamId]/chat`. Ekte privat — kun rostrede spillere, INGEN unntak for org-eier/admin (bevisst avvik fra appens vanlige autorisasjonsmønster). `user_is_rostered_on_team` i `app/team_authz.py`. | +| Offentlig runde-feed («Banter Board») | ✅ LIVE | Egen seksjon nederst på `/t/[id]`. Lesing gjenbruker `tournament.visibility`-mønsteret (ADR-018) uendret, inkl. anonym tilgang for `public`-synlige turneringer. Posting er strengere: krever innlogging OG org-medlemskap/deltakelse. | +| Bilder i feed/chat | ✅ LIVE | Med fra start (ikke utsatt) — gjenbruker `app/storage.py` sin MinIO+AVIF-pipeline fra ADR-018 uendret. | +| Sanntid-levering | ✅ LIVE | WebSockets, ikke polling — løser samtidig det tidligere åpne "sanntid vs. polling"-spørsmålet generelt. In-memory tilkoblingsregister per prosess (kun trygt med dagens ene `teecup_api`-container, se ARCHITECTURE_DECISIONS.md "Åpne spørsmål"). Egen Caddy-rute `/ws/*` rett til `teecup_api`. | +| Video | 💤 | Fortsatt utsatt, egen ADR om/når etterspurt. | +| 1-til-1 direktemeldinger | 💤 | Fortsatt utsatt, ikke etterspurt. | +| Moderering (offentlig feed) | ✅ LIVE | Forfatteren selv, ELLER org-eier/admin, kan slette et innlegg. Lag-chatten har ingen moderering utover forfatteren (rommet er privat, org-admin har uansett ikke lesetilgang). | + +Se ARCHITECTURE_DECISIONS.md ADR-025 for alle fire hovedbeslutningene og +CLAUDE.md-status for full byggerunde (datamodell, autorisasjon, scratch- +verifisering med 20 automatiserte sjekker inkl. reell WebSocket-sanntid, +og utrulling). ---