Kommunikasjon er live — det største enkeltløftet i prosjektet så langt:

Lag-chat («det hemmelige rommet»), tilgjengelig via en ny chat-knapp på hvert lagkort i lag/spillere-skjermen — ekte privat, org-eier/admin har ikke tilgang (verifisert helt ned til selve WebSocket-håndtrykket, ikke bare REST).
Offentlig runde-feed («Banter Board»), ny seksjon nederst på turneringens offentlige side — gjenbruker eksisterende synlighetsnivåer, men posting krever innlogging + tilknytning til turneringen/organisasjonen.
Bilder i begge, sanntid via WebSockets i begge.
Underveis: fant at Next.js ikke proxyer WebSocket-oppgraderinger pålitelig, løst med en egen Caddy-rute rett til API-et (samme mønster som media-ruten fra MinIO-runden). Alt scratch-verifisert grundig (20 automatiserte sjekker inkl. faktisk sanntidsmottak over en åpen WebSocket, ikke bare REST-svar), og bekreftet på ekte produksjon med et reelt wss://-håndtrykk over https.

Viktig å huske til neste økt: Caddy-endringen ligger uncommitted i det separate /opt/teeoff-repoet — samme fallgruve som tidligere Caddy-runder.

Alt er dokumentert i .md-filene. Naturlig neste kandidat er tilskuer-rollen (som Kommunikasjon-arbeidet nå gjør mulig å definere skikkelig), men si fra hva du vil prioritere.
This commit is contained in:
Erol Haagenrud 2026-07-19 22:33:13 +02:00
parent 20ae4fe6ea
commit 6774cec72b
4 changed files with 137 additions and 19 deletions

View file

@ -286,7 +286,13 @@
"Bash(curl -s -o /dev/null -w \"teeoff.no: %{http_code}\\\\n\" https://teeoff.no)", "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(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(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": [ "additionalDirectories": [
"/opt/teeoff/deploy", "/opt/teeoff/deploy",

View file

@ -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 er privat, org-admin har uansett ikke lesetilgang og kan derfor ikke
moderere det). 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 verifisering og utrulling, inkl. en egen Caddy-rute (`/ws/*`) for
WebSocket-trafikk direkte til `teecup_api` (Next.js sin `rewrites()` WebSocket-trafikk direkte til `teecup_api` (Next.js sin `rewrites()`
proxyer ikke WebSocket-oppgraderinger pålitelig — samme klasse proxyer ikke WebSocket-oppgraderinger pålitelig — samme klasse

116
CLAUDE.md
View file

@ -1480,14 +1480,118 @@ Ferdig og verifisert:
begge containere boot-et rent, `/health` og `/dashboard` → 200, begge containere boot-et rent, `/health` og `/dashboard` → 200,
`teeoff.no` upåvirket. `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: Neste steg:
1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet. 1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
2. Kommunikasjon (chat/feed) — ikke startet. 2. Fortsatt åpne beslutninger fra FEATURE_BACKLOG.md: tilskuer-rolle (bevisst
3. Fortsatt åpne beslutninger fra FEATURE_BACKLOG.md: sanntid/WebSockets vs. utsatt til nå at Kommunikasjon er ferdig — bør tas fatt på snart, samme
polling, tilskuer-rolle (bevisst utsatt til Kommunikasjon-runden), begrunnelse som før: definer sammen med feed-synligheten, som nå finnes),
kode-regenerering for ADR-020, turnering-status (draft/active/completed/ kode-regenerering for ADR-020, korrigering-godkjenning fra motpart, live
archived) via API, korrigering-godkjenning fra motpart. leaderboard (samme WebSocket-mekanisme som meldinger kan gjenbrukes, ikke
4. Ved fremtidige nye V0-skjermer/-design i V0 (fortsett i samme prosjekt), 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 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 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 V0-skjermen bygger inn handlinger backend ikke støtter ennå FØR

View file

@ -464,10 +464,12 @@
- «BREAKING: X vant matchen». PWA push. Egen infrastruktur-bit. - «BREAKING: X vant matchen». PWA push. Egen infrastruktur-bit.
### Sanntid (WebSockets) ### Sanntid (WebSockets)
- **Status:** ❓ trenger beslutning - **Status:** ✅ mekanisme LIVE 2026-07-19 (ADR-025), men KUN koblet til
- Live leaderboard og chat som oppdateres uten refresh. Vi har leaderboard- meldinger ennå. WebSockets valgt (ikke polling) — se Kommunikasjon-
backend-endepunktet (se egen seksjon over), men ikke sanntidsleveringen seksjonen. `app/routers/messaging.py` sitt in-memory tilkoblingsregister
eller frontend-skjermen ennå. Valg: WebSockets vs. polling. 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) ### Knockout / cup-turnering (egen turneringstype)
- **Status:** 💤 utsatt (egen fremtidig type, ADR-011) - **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 | | Del | Status | Notat |
|---|---|---| |---|---|---|
| Lag-intern chat («det hemmelige rommet») | 🔨 | Bekreftet ønsket. Kanal m/ scope `team`. | | 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») | ❓ | 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. | | 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 | 📋 | v1. Objektlagring (MinIO), presigned opplasting. | | Bilder i feed/chat | ✅ LIVE | Med fra start (ikke utsatt) — gjenbruker `app/storage.py` sin MinIO+AVIF-pipeline fra ADR-018 uendret. |
| Video | 💤 | Arkitekt for det, bygg senere (ADR-forslag). | | 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`. |
| 1-til-1 direktemeldinger | 💤 | Gemini frarådet for v1; ikke etterspurt av deg. | | Video | 💤 | Fortsatt utsatt, egen ADR om/når etterspurt. |
| Moderering (for offentlig innhold) | ❓ | Kreves hvis «alle med lenken». Mønster finnes i teeoff. | | 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).
--- ---