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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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…" />
|
||||
|
||||
Reference in New Issue
Block a user