teecup/frontend/components/dashboard.tsx

699 lines
23 KiB
TypeScript
Raw Normal View History

"use client"
import type React from "react"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
Update Todos Flytte is_participant-logikk til team_authz.py (unngå sirkulær import) Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup Widen tournaments.py: list_sessions/list_teams/concede_tournament Widen courses.py: list_holes Widen messaging.py: team chat REST-endepunkter (list/send/delete) Legge til my_session_id/my_match_id i /auth/me sin my_tournaments Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder» Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller) Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster) Bygge HCP-historikk over tid Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort: Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder". Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match). Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
import {
Building2,
Calendar,
ChevronRight,
ChevronsUpDown,
ClipboardList,
LogOut,
MessageCircle,
Plus,
Trophy,
Users,
UserCircle,
X,
Check,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Wordmark } from "@/components/wordmark"
import { TournamentCard, type Tournament } from "@/components/tournament-card"
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
import { TournamentStatusBadge, type TournamentStatus } from "@/components/tournament-status-badge"
import { cn } from "@/lib/utils"
type MyOrg = { organization_id: string; name: string; role: string }
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
// ADR-031: "Mine runder" -- turneringer brukeren er SPILLER i (rostret på et
// lag), på tvers av organisasjoner, uavhengig av organisasjonsmedlemskap.
type MyTournament = {
organization_id: string
organization_name: string
tournament_id: string
tournament_name: string
status: TournamentStatus
team_id: string
team_name: string
team_color: string | null
next_session_at: string | null
Update Todos Flytte is_participant-logikk til team_authz.py (unngå sirkulær import) Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup Widen tournaments.py: list_sessions/list_teams/concede_tournament Widen courses.py: list_holes Widen messaging.py: team chat REST-endepunkter (list/send/delete) Legge til my_session_id/my_match_id i /auth/me sin my_tournaments Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder» Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller) Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster) Bygge HCP-historikk over tid Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort: Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder". Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match). Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
// 2026-07-21 (deltaker-tilgang-runden): en av brukerens egne matcher, hvis
// noen finnes -- lar kortet lenke direkte til lagets chat/scorekort.
my_session_id: string | null
my_match_id: string | null
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
}
type Me = {
id: string
email: string
display_name: string
preferred_locale: string
organizations: MyOrg[]
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
my_tournaments: MyTournament[]
}
type ApiTournament = {
id: string
name: string
status: TournamentStatus
start_date: string | null
end_date: string | null
}
function toTournament(t: ApiTournament): Tournament {
return {
id: t.id,
name: t.name,
status: t.status,
startDate: t.start_date ?? undefined,
endDate: t.end_date ?? undefined,
}
}
// --- Component -------------------------------------------------------------
export function Dashboard() {
const router = useRouter()
const [me, setMe] = useState<Me | null>(null)
const [loadingMe, setLoadingMe] = useState(true)
const [activeOrgId, setActiveOrgId] = useState<string | null>(null)
const [tournaments, setTournaments] = useState<Tournament[]>([])
const [loadingTournaments, setLoadingTournaments] = useState(false)
const [error, setError] = useState<string | null>(null)
// Hent innlogget bruker + organisasjonsmedlemskap. Ingen gyldig sesjon ->
// tilbake til innloggingssiden (denne siden krever auth).
useEffect(() => {
let cancelled = false
async function loadMe() {
try {
const res = await fetch("/auth/me", { credentials: "include" })
if (!res.ok) {
router.replace("/")
return
}
const data: Me = await res.json()
if (cancelled) return
setMe(data)
setActiveOrgId(data.organizations[0]?.organization_id ?? null)
} catch {
if (!cancelled) router.replace("/")
} finally {
if (!cancelled) setLoadingMe(false)
}
}
void loadMe()
return () => {
cancelled = true
}
}, [router])
// Turneringer hentes per valgt organisasjon, ikke i /auth/me-kallet.
useEffect(() => {
if (!activeOrgId) {
setTournaments([])
return
}
let cancelled = false
async function loadTournaments() {
setLoadingTournaments(true)
setError(null)
try {
const res = await fetch(`/orgs/${activeOrgId}/tournaments`, { credentials: "include" })
if (!res.ok) throw new Error(`tournaments: ${res.status}`)
const data: ApiTournament[] = await res.json()
if (!cancelled) setTournaments(data.map(toTournament))
} catch {
if (!cancelled) setError("Klarte ikke å hente turneringer. Prøv igjen om litt.")
} finally {
if (!cancelled) setLoadingTournaments(false)
}
}
void loadTournaments()
return () => {
cancelled = true
}
}, [activeOrgId])
async function handleCreateOrg(name: string) {
setError(null)
try {
const res = await fetch("/orgs", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ name }),
})
if (!res.ok) throw new Error(`orgs: ${res.status}`)
const org: { id: string; name: string; role: string } = await res.json()
setMe((prev) =>
prev
? {
...prev,
organizations: [
...prev.organizations,
{ organization_id: org.id, name: org.name, role: org.role },
],
}
: prev,
)
setActiveOrgId(org.id)
} catch {
setError("Klarte ikke å opprette organisasjonen. Prøv igjen.")
}
}
async function handleCreateTournament(name: string) {
if (!activeOrgId) return
setError(null)
try {
const res = await fetch(`/orgs/${activeOrgId}/tournaments`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ name }),
})
if (!res.ok) throw new Error(`create tournament: ${res.status}`)
const created: ApiTournament = await res.json()
setTournaments((prev) => [toTournament(created), ...prev])
} catch {
setError("Klarte ikke å opprette turneringen. Prøv igjen.")
}
}
async function handleLogout() {
try {
await fetch("/auth/logout", { method: "POST", credentials: "include" })
} finally {
router.replace("/")
}
}
if (loadingMe) {
return (
<div className="flex min-h-[100dvh] flex-col items-center justify-center gap-4 bg-background">
<div
aria-hidden="true"
className="size-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary"
/>
</div>
)
}
if (!me) return null // router.replace("/") er allerede utløst
const activeOrg = me.organizations.find((o) => o.organization_id === activeOrgId) ?? null
const hasOrg = me.organizations.length > 0 && activeOrg !== null
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
const hasMyTournaments = me.my_tournaments.length > 0
return (
<div className="flex min-h-[100dvh] flex-col bg-background">
<header className="sticky top-0 z-10 border-b border-border bg-background/80 backdrop-blur">
<div className="mx-auto flex w-full max-w-3xl items-center justify-between gap-4 px-5 py-4">
<Wordmark compact />
<div className="flex items-center gap-1">
<Link
href="/account"
className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-sm font-semibold text-muted-foreground transition-colors hover:text-foreground"
>
<UserCircle aria-hidden="true" className="size-4" />
Konto
</Link>
<button
type="button"
onClick={handleLogout}
className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-sm font-semibold text-muted-foreground transition-colors hover:text-foreground"
>
<LogOut aria-hidden="true" className="size-4" />
Logg ut
</button>
</div>
</div>
</header>
<main className="mx-auto w-full max-w-3xl flex-1 px-5 py-8 sm:py-10">
{error && (
<p role="alert" className="mb-4 text-sm font-medium text-destructive">
{error}
</p>
)}
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
<div className="flex flex-col gap-8">
{hasMyTournaments && <MyToursSection tournaments={me.my_tournaments} />}
{hasOrg && activeOrg ? (
<OrganizationView
org={activeOrg}
orgs={me.organizations}
tournaments={tournaments}
loadingTournaments={loadingTournaments}
onSelectOrg={setActiveOrgId}
onCreateTournament={handleCreateTournament}
onCreateOrg={handleCreateOrg}
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
/>
) : (
<CreateOrganizationState onCreate={handleCreateOrg} compact={hasMyTournaments} />
)}
</div>
</main>
</div>
)
}
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
// --- "Mine runder" (ADR-031) -----------------------------------------------
// Turneringer brukeren er SPILLER i, uavhengig av organisasjonsmedlemskap.
Update Todos Flytte is_participant-logikk til team_authz.py (unngå sirkulær import) Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup Widen tournaments.py: list_sessions/list_teams/concede_tournament Widen courses.py: list_holes Widen messaging.py: team chat REST-endepunkter (list/send/delete) Legge til my_session_id/my_match_id i /auth/me sin my_tournaments Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder» Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller) Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster) Bygge HCP-historikk over tid Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort: Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder". Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match). Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
// Kortets hoveddel lenker til den offentlige turnering-siden. Fra
// 2026-07-21 (deltaker-tilgang-runden) har kortet i tillegg en
// handlingsrad med lenker RETT til lagets private chat og (hvis brukeren
// har en match) scorekortet -- begge de org-scopede sidene, som nå
// fungerer for en rostret/påmeldt spiller UTEN organisasjonsmedlemskap
// (se team_authz.py sin user_is_rostered_on_team/user_is_match_participant,
// og den nye is_org_member-ELLER-user_is_tournament_participant-unionen på
// de øvrige org-scopede GET-endepunktene siden bruker).
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
function MyToursSection({ tournaments }: { tournaments: MyTournament[] }) {
return (
<section className="flex flex-col gap-3" aria-label="Mine runder">
<h2 className="text-sm font-bold uppercase tracking-wide text-muted-foreground">Mine runder</h2>
<div className="flex flex-col gap-3">
Update Todos Flytte is_participant-logikk til team_authz.py (unngå sirkulær import) Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup Widen tournaments.py: list_sessions/list_teams/concede_tournament Widen courses.py: list_holes Widen messaging.py: team chat REST-endepunkter (list/send/delete) Legge til my_session_id/my_match_id i /auth/me sin my_tournaments Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder» Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller) Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster) Bygge HCP-historikk over tid Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort: Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder". Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match). Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
{tournaments.map((t) => {
const orgParam = `org=${t.organization_id}&name=${encodeURIComponent(t.tournament_name)}`
return (
<div
key={`${t.organization_id}-${t.tournament_id}`}
className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 shadow-sm shadow-black/5 transition-colors hover:border-primary/50 sm:p-5"
>
<Link href={`/t/${t.tournament_id}`} className="group flex items-center gap-4 text-left">
<span
aria-hidden="true"
className="size-3 shrink-0 rounded-full"
style={{ backgroundColor: t.team_color ?? "#64748b" }}
/>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
<h3 className="truncate text-base font-bold text-foreground">{t.tournament_name}</h3>
<TournamentStatusBadge status={t.status} />
</div>
<span className="truncate text-sm text-muted-foreground">
{t.organization_name} · {t.team_name}
</span>
{t.next_session_at && (
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
<Calendar aria-hidden="true" className="size-4 shrink-0" />
<span className="tabular-nums">{formatNextSession(t.next_session_at)}</span>
</div>
)}
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
</div>
Update Todos Flytte is_participant-logikk til team_authz.py (unngå sirkulær import) Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup Widen tournaments.py: list_sessions/list_teams/concede_tournament Widen courses.py: list_holes Widen messaging.py: team chat REST-endepunkter (list/send/delete) Legge til my_session_id/my_match_id i /auth/me sin my_tournaments Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder» Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller) Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster) Bygge HCP-historikk over tid Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort: Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder". Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match). Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
<ChevronRight
aria-hidden="true"
className="size-5 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5 group-hover:text-foreground"
/>
</Link>
<div className="flex flex-wrap gap-2 border-t border-border pt-3">
<Link
href={`/tournaments/${t.tournament_id}/teams/${t.team_id}/chat?${orgParam}`}
className="inline-flex h-9 items-center gap-1.5 rounded-xl border border-border bg-background px-3 text-sm font-semibold text-foreground transition-colors hover:bg-accent/50"
>
<MessageCircle aria-hidden="true" className="size-4" />
Lag-chat
</Link>
{t.my_match_id && t.my_session_id && (
<Link
href={`/tournaments/${t.tournament_id}/sessions/${t.my_session_id}/matches/${t.my_match_id}?${orgParam}`}
className="inline-flex h-9 items-center gap-1.5 rounded-xl border border-border bg-background px-3 text-sm font-semibold text-foreground transition-colors hover:bg-accent/50"
>
<ClipboardList aria-hidden="true" className="size-4" />
Scorekort
</Link>
)}
</div>
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
</div>
Update Todos Flytte is_participant-logikk til team_authz.py (unngå sirkulær import) Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup Widen tournaments.py: list_sessions/list_teams/concede_tournament Widen courses.py: list_holes Widen messaging.py: team chat REST-endepunkter (list/send/delete) Legge til my_session_id/my_match_id i /auth/me sin my_tournaments Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder» Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller) Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster) Bygge HCP-historikk over tid Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort: Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder". Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match). Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
)
})}
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
</div>
</section>
)
}
function formatNextSession(iso: string) {
const date = new Date(iso)
if (Number.isNaN(date.getTime())) return iso
return new Intl.DateTimeFormat("no-NO", {
weekday: "short",
day: "numeric",
month: "short",
hour: "2-digit",
minute: "2-digit",
}).format(date)
}
// --- State A: no organization ---------------------------------------------
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
function CreateOrganizationState({
onCreate,
compact = false,
}: {
onCreate: (name: string) => void
compact?: boolean
}) {
const [name, setName] = useState("")
const valid = name.trim().length >= 2
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!valid) return
onCreate(name.trim())
setName("")
}
return (
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
<div
className={cn(
"mx-auto flex max-w-md flex-col items-center text-center",
compact ? "gap-4" : "gap-6 pt-6 sm:pt-12",
)}
>
{!compact && (
<div className="flex size-16 items-center justify-center rounded-2xl bg-primary/15">
<Building2 aria-hidden="true" className="size-8 text-primary" />
</div>
)}
<div className="flex flex-col gap-2">
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
<h1 className={cn("font-extrabold tracking-tight text-foreground text-balance", compact ? "text-lg" : "text-2xl")}>
{compact ? "Opprett din egen organisasjon" : "Du har ingen organisasjon ennå"}
</h1>
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
Update Todos Migrasjon 015: app_user-profilfelt + player_organizations_for_user()-bro Backend: utvid Me + PATCH /auth/profile + avatar-opplasting/sletting Backend: 'mine runder'-data + check_visibility-utvidelse for deltakere Frontend: profil-seksjon i /account Frontend: 'Mine runder'-seksjon + betinget tom-tilstand i dashboard.tsx Scratch-verifisere alt (15 sjekker bestått) Typesjekket frontend-build ADR-031 + .md-oppdatering Bygget og scratch-verifisert (15 automatiserte sjekker). Oppsummering: Personlig profil — nye felt på selve kontoen (ikke på org-ens spillerdata, det er bevisst holdt atskilt siden en person kan ha ulike spiller-rader i ulike klubber): profilbilde, fornavn, etternavn, fødselsdato, kjønn, HCP, hjemmeklubb. Redigeres i en ny seksjon på /account. Sletting av enkeltfelt fungerer (send tomt/null), profilbilde kan lastes opp og fjernes. "Mine runder" — ny seksjon øverst på dashbordet, viser turneringer du er rostret i på tvers av alle organisasjoner, uavhengig av om du er medlem noe sted. Et reelt sikkerhetshull jeg fant underveis, ikke antatt på forhånd: da jeg testet "Mine runder" mot en faktisk ren spiller (ingen organisasjonsmedlemskap), oppdaget jeg at synlighetsregelen kun ga deltakere tilgang for det strengeste synlighetsnivået — ikke for standard-nivået («org»), som er det ALLE nye turneringer får automatisk. En ren spiller ville altså vært låst ute av sin egen, helt normale turnering. Fikset og verifisert grundig at det er en ren utvidelse: en fremmed innlogget bruker og en anonym leser blir fortsatt korrekt avvist som før. Bevisst utenfor omfang, tydelig flagget: "Mine runder" lenker til den offentlige turnering-siden, ikke til lagets private chat eller scorekortet ennå — de krever fortsatt ekte organisasjonsmedlemskap, en strengere sperre brukt bredt i hele appen som jeg ikke ville endre uten en egen, forsiktig runde. Notert som naturlig neste steg. Ingen kode for punkt 2 (midlertidige spillere) i denne runden, som avtalt.
2026-07-20 10:41:30 +02:00
{compact
? "Skal du selv arrangere turneringer? En organisasjon er golfklubben eller bedriften som står bak."
: "En organisasjon er golfklubben eller bedriften som arrangerer turneringen. Opprett en for å komme i gang."}
</p>
</div>
<form
onSubmit={handleSubmit}
className="flex w-full flex-col gap-4 rounded-3xl border border-border bg-card p-6 text-left shadow-lg shadow-black/5"
>
<div className="flex flex-col gap-2">
<Label htmlFor="org-name" className="text-sm font-semibold">
Navn organisasjon
</Label>
<Input
id="org-name"
autoFocus
placeholder="F.eks. Oslo Golfklubb"
value={name}
onChange={(e) => setName(e.target.value)}
className="h-14 rounded-2xl text-base"
/>
</div>
<Button
type="submit"
disabled={!valid}
className="h-14 rounded-2xl text-base font-bold shadow-sm"
>
Opprett organisasjon
</Button>
</form>
</div>
)
}
// --- State B: has organization(s) -----------------------------------------
function OrganizationView({
org,
orgs,
tournaments,
loadingTournaments,
onSelectOrg,
onCreateTournament,
onCreateOrg,
}: {
org: MyOrg
orgs: MyOrg[]
tournaments: Tournament[]
loadingTournaments: boolean
onSelectOrg: (id: string) => void
onCreateTournament: (name: string) => void
onCreateOrg: (name: string) => void
}) {
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
{orgs.length > 1 ? (
<OrganizationSwitcher org={org} orgs={orgs} onSelectOrg={onSelectOrg} />
) : (
<div className="flex items-center gap-3">
<div className="flex size-11 items-center justify-center rounded-xl bg-primary/15">
<Building2 aria-hidden="true" className="size-5 text-primary" />
</div>
<div className="flex flex-col">
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Organisasjon
</span>
<span className="text-lg font-extrabold tracking-tight text-foreground">
{org.name}
</span>
</div>
</div>
)}
<div className="flex flex-wrap items-center gap-2">
<Link
href={`/organizations/${org.organization_id}/members?name=${encodeURIComponent(org.name)}`}
className="inline-flex h-11 items-center gap-1.5 rounded-2xl border border-border bg-card px-4 text-sm font-semibold text-foreground transition-colors hover:bg-accent/50"
>
<Users aria-hidden="true" className="size-4" />
Medlemmer
</Link>
<NewOrganizationControl onCreate={onCreateOrg} />
<NewTournamentControl onCreate={onCreateTournament} />
</div>
</div>
<section className="flex flex-col gap-3" aria-label="Turneringer">
{loadingTournaments ? (
<div className="flex justify-center py-12">
<div
aria-hidden="true"
className="size-8 animate-spin rounded-full border-4 border-primary/20 border-t-primary"
/>
</div>
) : tournaments.length > 0 ? (
tournaments.map((t) => (
<TournamentCard key={t.id} tournament={t} orgId={org.organization_id} />
))
) : (
<EmptyTournamentState orgName={org.name} />
)}
</section>
</div>
)
}
function OrganizationSwitcher({
org,
orgs,
onSelectOrg,
}: {
org: MyOrg
orgs: MyOrg[]
onSelectOrg: (id: string) => void
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center gap-3 rounded-2xl border border-border bg-card px-3 py-2.5 text-left shadow-sm shadow-black/5 transition-colors hover:bg-accent/50">
<div className="flex size-11 items-center justify-center rounded-xl bg-primary/15">
<Building2 aria-hidden="true" className="size-5 text-primary" />
</div>
<div className="flex flex-col">
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Organisasjon
</span>
<span className="text-lg font-extrabold tracking-tight text-foreground">
{org.name}
</span>
</div>
<ChevronsUpDown aria-hidden="true" className="ml-1 size-4 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-64 rounded-2xl p-1.5">
{orgs.map((o) => (
<DropdownMenuItem
key={o.organization_id}
onClick={() => onSelectOrg(o.organization_id)}
className="flex cursor-pointer items-center justify-between gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold"
>
{o.name}
{o.organization_id === org.organization_id && (
<Check aria-hidden="true" className="size-4 text-primary" />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
// Ingen vei fantes tidligere til å opprette en ANDRE organisasjon når man
// allerede har én -- CreateOrganizationState (under) vises kun ved null
// org-er. Backend (POST /orgs) har ingen begrensning på antall org-er én
// bruker kan eie (ADR-021), så dette var en ren UI-mangel.
function NewOrganizationControl({ onCreate }: { onCreate: (name: string) => void }) {
const [open, setOpen] = useState(false)
const [name, setName] = useState("")
const valid = name.trim().length >= 2
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!valid) return
onCreate(name.trim())
setName("")
setOpen(false)
}
if (!open) {
return (
<Button
type="button"
variant="outline"
onClick={() => setOpen(true)}
className="inline-flex h-11 items-center gap-1.5 rounded-2xl border border-border bg-card px-4 text-sm font-semibold text-foreground transition-colors hover:bg-accent/50"
>
<Building2 aria-hidden="true" className="size-4" />
Ny organisasjon
</Button>
)
}
return (
<form
onSubmit={handleSubmit}
className="flex w-full items-center gap-2 rounded-2xl border border-border bg-card p-2 shadow-sm shadow-black/5 sm:w-auto"
>
<Input
autoFocus
placeholder="Navn på organisasjon"
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Escape") setOpen(false)
}}
className="h-10 min-w-0 flex-1 rounded-xl text-base sm:w-56"
/>
<Button
type="submit"
disabled={!valid}
size="icon"
className="size-10 shrink-0 rounded-xl"
aria-label="Opprett organisasjon"
>
<Check aria-hidden="true" className="size-5" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setOpen(false)}
className="size-10 shrink-0 rounded-xl text-muted-foreground"
aria-label="Avbryt"
>
<X aria-hidden="true" className="size-5" />
</Button>
</form>
)
}
function NewTournamentControl({ onCreate }: { onCreate: (name: string) => void }) {
const [open, setOpen] = useState(false)
const [name, setName] = useState("")
const valid = name.trim().length >= 2
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!valid) return
onCreate(name.trim())
setName("")
setOpen(false)
}
if (!open) {
return (
<Button
type="button"
onClick={() => setOpen(true)}
className="h-12 shrink-0 rounded-2xl text-base font-bold shadow-sm"
>
<Plus aria-hidden="true" className="size-5" />
Ny turnering
</Button>
)
}
return (
<form
onSubmit={handleSubmit}
className="flex w-full items-center gap-2 rounded-2xl border border-border bg-card p-2 shadow-sm shadow-black/5 sm:w-auto"
>
<Input
autoFocus
placeholder="Navn på turnering"
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Escape") setOpen(false)
}}
className="h-10 min-w-0 flex-1 rounded-xl text-base sm:w-56"
/>
<Button
type="submit"
disabled={!valid}
size="icon"
className="size-10 shrink-0 rounded-xl"
aria-label="Opprett turnering"
>
<Check aria-hidden="true" className="size-5" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setOpen(false)}
className="size-10 shrink-0 rounded-xl text-muted-foreground"
aria-label="Avbryt"
>
<X aria-hidden="true" className="size-5" />
</Button>
</form>
)
}
function EmptyTournamentState({ orgName }: { orgName: string }) {
return (
<div className="flex flex-col items-center gap-3 rounded-3xl border border-dashed border-border bg-card/50 px-6 py-12 text-center">
<div className="flex size-14 items-center justify-center rounded-2xl bg-muted">
<Trophy aria-hidden="true" className="size-7 text-muted-foreground" />
</div>
<div className="flex flex-col gap-1">
<h2 className="text-base font-bold text-foreground text-balance">
Ingen turneringer ennå i {orgName}
</h2>
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
Trykk «Ny turnering» for å opprette den første.
</p>
</div>
</div>
)
}