Apre lo Scout Live dalla pagina partita e a tutta la squadra.

Tolto dalla home e poi da Squadra, `ScoutEntry` era rimasto orfano:
`/scout` si raggiungeva solo scrivendo l'URL a mano. Ora la card sta
nella sezione «Scout live» di `/partita/$id`. La prop `eventoId` la
accende solo se la partita aperta è quella di oggi: senza, da una
partita futura o passata si sarebbe finiti sullo scout di un'altra.

Cade anche la riserva agli admin, in `ScoutEntry` e nella route: può
scoutare chiunque sia autenticato, uno per volta grazie al lock di
sessione. È anche l'unico controllo che c'era, visto che le policy RLS
sono sempre state aperte a tutti gli autenticati.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 00:24:39 +02:00
co-authored by Claude Opus 5
parent 73902cc0a4
commit 0c136f8f53
5 changed files with 51 additions and 41 deletions
+9
View File
@@ -6,6 +6,15 @@ qui: sta in [ROADMAP.md](ROADMAP.md).
## Versione attuale — agosto 2026
### Lo Scout Live si apre dalla pagina della partita
- Tolto dalla home, era rimasto senza nessun link: `/scout` si raggiungeva solo scrivendo
l'URL. Ora la card `ScoutEntry` sta in `/partita/$id`, sezione «Scout live».
- Si accende solo se la partita aperta è quella di oggi (nuova prop `eventoId`), altrimenti
resta grigia con «Si attiva il giorno della partita». Lock di sessione invariato.
- Non è più riservato agli admin: può scoutare chiunque sia autenticato, uno per volta grazie
al lock. Sparisce il messaggio «Scout riservato».
### Il sondaggio pre-partita apre alle 8:00 del giorno della partita
- Prima era sempre votabile, anche settimane prima: ora la card resta chiusa con l'avviso di
+17 -6
View File
@@ -30,10 +30,21 @@ aggiornato da qualunque dispositivo.
## Chi può usarlo
Solo gli admin lato UI: `ScoutEntry.tsx` e `scout.tsx` bloccano i non-admin con il messaggio
"Scout riservato". **Il controllo non è imposto a livello database**: le policy RLS di
`scout_sessioni`/`scout_live`/`scout_partite` sono aperte a qualunque utente autenticato, non
solo agli admin — la migration M4 toglie l'accesso solo al ruolo `anon`.
Chiunque sia autenticato: non è più riservato agli admin. A tenere l'ordine basta il lock di
sessione — scoutizza uno per volta, gli altri vedono "In uso da …". Questo allinea l'interfaccia
alle policy RLS di `scout_sessioni`/`scout_live`/`scout_partite`, che sono sempre state aperte a
qualunque utente autenticato (la migration M4 toglie l'accesso solo al ruolo `anon`).
---
## Da dove ci si arriva
`ScoutEntry.tsx` è l'unico accesso a `/scout`: sta nella pagina della partita
(`partita.$id.tsx`, sezione «Scout live»), visibile a tutta la squadra. Si accende solo se
**quella** partita è quella di oggi — la prop `eventoId` confronta l'evento aperto con
`partitaDiOggi()` — e se nessun altro ha il lock; negli altri casi resta una card grigia non
cliccabile («Si attiva il giorno della partita» / «In uso da …»). Dalla home è stato tolto
perché occupava spazio 6 giorni su 7.
---
@@ -104,7 +115,8 @@ calcolati a runtime).
## Limiti noti
- Controllo "solo admin" non imposto dal database (vedi sopra).
- Scout aperto a tutta la squadra: nessun filtro su chi può registrare le azioni, l'unica
garanzia è il lock di sessione (vedi sopra).
- Possibile, per quanto improbabile, doppio "successo" applicativo nel prendere il lock:
lettura e upsert non sono atomici.
- `scout_partite` si inserisce ma non si corregge dall'interfaccia: solo eliminazione totale.
@@ -116,4 +128,3 @@ calcolati a runtime).
## Evoluzioni possibili
- Realtime (Supabase Realtime) per aggiornare la sessione condivisa senza refresh manuale.
- Restringere le policy RLS al solo ruolo admin.
+20 -23
View File
@@ -3,36 +3,33 @@ import { ChevronRight, Lock, Radio } from "lucide-react";
import { cn } from "@/lib/utils";
import { sessioneScaduta, usePartitaDiOggi, useSessioneScout } from "@/lib/scout-live";
import { useGiocatoreCorrente } from "@/lib/user-store";
import { useIsAdmin } from "@/lib/ruoli";
/** Accesso allo scout live: attivo solo il giorno della partita e se nessun altro lo sta usando. */
export function ScoutEntry({ variante = "grande" }: { variante?: "grande" | "compatto" }) {
const { pronto, partita } = usePartitaDiOggi();
/**
* Accesso allo scout live: attivo solo il giorno della partita e se nessun altro lo sta usando.
* Con `eventoId` si accende solo se quella partita è proprio quella di oggi.
*/
export function ScoutEntry({
variante = "grande",
eventoId,
}: {
variante?: "grande" | "compatto";
eventoId?: string;
}) {
const { pronto, partita: diOggi } = usePartitaDiOggi();
const partita = eventoId && diOggi?.id !== eventoId ? null : diOggi;
const io = useGiocatoreCorrente();
const admin = useIsAdmin();
const { data: sessione } = useSessioneScout(partita?.id ?? null);
// Strumento tecnico: solo i referenti/allenatori scoutizzano la partita.
const abilitato = admin;
const attiva = sessione && !sessioneScaduta(sessione) ? sessione : null;
const occupato = !!attiva && attiva.giocatore_id !== io?.id;
const disponibile = abilitato && pronto && !!partita && !occupato;
const disponibile = pronto && !!partita && !occupato;
const titolo = !abilitato
? "Scout live"
: !partita
? "Scout live non attivo"
: occupato
? "Scout occupato"
: "Scout live";
const sottotitolo = !abilitato
? "Riservato ad allenatori e referenti"
: !partita
? "Si attiva il giorno della partita"
: occupato
? `In uso da ${attiva!.giocatore_nome}`
: "Segna punti, ace e muri in tempo reale";
const titolo = !partita ? "Scout live non attivo" : occupato ? "Scout occupato" : "Scout live";
const sottotitolo = !partita
? "Si attiva il giorno della partita"
: occupato
? `In uso da ${attiva!.giocatore_nome}`
: "Segna punti, ace e muri in tempo reale";
const contenuto = (
<>
+5
View File
@@ -9,6 +9,7 @@ import { useCsi } from "@/lib/csi";
import { matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
import { Pagelle } from "@/components/crapp/Pagelle";
import { SondaggioCacche } from "@/components/crapp/SondaggioCacche";
import { ScoutEntry } from "@/components/crapp/ScoutEntry";
import { useScoutMatches, totaliPerGiocatore, totaliSquadra } from "@/lib/scout-store";
import { csvScoutMatch, scaricaCsv } from "@/lib/scout-export";
import { useGiocatoreCorrente } from "@/lib/user-store";
@@ -204,6 +205,10 @@ function PartitaDetail() {
</Section>
)}
<Section titolo="Scout live">
<ScoutEntry eventoId={evento.id} />
</Section>
<Section titolo="Sondaggio pre-partita">
<SondaggioCacche eventoId={evento.id} dataEvento={evento.data} />
</Section>
-12
View File
@@ -8,7 +8,6 @@ import { formatData } from "@/lib/crapp-data";
import type { Evento } from "@/lib/eventi";
import { useRosa } from "@/lib/rosa";
import { useGiocatoreCorrente } from "@/lib/user-store";
import { useIsAdmin } from "@/lib/ruoli";
import { usePresenzeEvento } from "@/lib/presenze";
import {
statoIniziale,
@@ -83,7 +82,6 @@ function Blocco({
function Scout() {
const { pronto, partita } = usePartitaDiOggi();
const io = useGiocatoreCorrente();
const admin = useIsAdmin();
const sessione = useSessioneScout(partita?.id ?? null);
const statoSalvato = useStatoScout(partita?.id ?? null);
const apri = useApriSessioneScout();
@@ -102,16 +100,6 @@ function Scout() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [controllo, partita?.id, io?.id]);
if (io && !admin) {
return (
<Blocco
icona={<Lock className="h-6 w-6" />}
titolo="Scout riservato"
testo="Lo scout live è uno strumento tecnico per allenatori e referenti della squadra."
/>
);
}
if (!pronto || sessione.isLoading || statoSalvato.isLoading) {
return (
<Blocco icona={<Radio className="h-6 w-6" />} titolo="Scout live" testo="Caricamento…" />