From 9ca124a868d20b9b7043c3808c63e3f66c80854d Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Sun, 30 Aug 2026 13:07:59 +0200 Subject: [PATCH] Expire a scout session whose timestamp cannot be read. sessioneScaduta compared Date.now() against NaN when aggiornato_il was not a valid date, and every comparison with NaN is false: the session was reported as still active, so the Scout Live table stayed locked to a player who could no longer release it. An unreadable timestamp now frees the session, which is the safe direction: at worst someone takes over a scouting session that was already unattended. Co-Authored-By: Claude Opus 5 --- src/lib/scout-live.ts | 16 ++++++++-- test/unit/scout-live.test.ts | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 test/unit/scout-live.test.ts diff --git a/src/lib/scout-live.ts b/src/lib/scout-live.ts index 0892123..c1712c3 100644 --- a/src/lib/scout-live.ts +++ b/src/lib/scout-live.ts @@ -24,7 +24,10 @@ export type SessioneScout = { export function sessioneScaduta(s: SessioneScout | null): boolean { if (!s) return true; - return Date.now() - new Date(s.aggiornato_il).getTime() > SCADENZA_MINUTI * 60_000; + const aggiornato = new Date(s.aggiornato_il).getTime(); + // Timestamp illeggibile: meglio liberare la sessione che lasciarla bloccata per sempre. + if (Number.isNaN(aggiornato)) return true; + return Date.now() - aggiornato > SCADENZA_MINUTI * 60_000; } const storageKey = (eventoId: string) => `crap-scout-session-${eventoId}`; @@ -82,7 +85,10 @@ export function useSessioneScout(eventoId: string | null) { } catch { const onStorage = (e: StorageEvent) => { if (e.key === storageKey(eventoId)) { - queryClient.setQueryData(SESSIONE_KEY(eventoId), e.newValue ? (JSON.parse(e.newValue) as SessioneScout) : null); + queryClient.setQueryData( + SESSIONE_KEY(eventoId), + e.newValue ? (JSON.parse(e.newValue) as SessioneScout) : null, + ); } }; window.addEventListener("storage", onStorage); @@ -140,7 +146,11 @@ export function useChiudiSessioneScout() { } /** Mantiene viva la sessione mentre lo scout รจ aperto. */ -export function useHeartbeatScout(eventoId: string | null, giocatoreId: string | null, attivo: boolean) { +export function useHeartbeatScout( + eventoId: string | null, + giocatoreId: string | null, + attivo: boolean, +) { useEffect(() => { if (!attivo || !eventoId || !giocatoreId || typeof window === "undefined") return; const id = window.setInterval(() => { diff --git a/test/unit/scout-live.test.ts b/test/unit/scout-live.test.ts new file mode 100644 index 0000000..6b31ed5 --- /dev/null +++ b/test/unit/scout-live.test.ts @@ -0,0 +1,58 @@ +/** Check della sessione Scout Live: `bun test/unit/scout-live.test.ts`. */ +import assert from "node:assert/strict"; +import type { Evento } from "@/lib/eventi"; +import { + SCADENZA_MINUTI, + dataOggi, + partitaDiOggi, + sessioneScaduta, + type SessioneScout, +} from "@/lib/scout-live"; + +const evento = (id: string, data: string, tipo: Evento["tipo"]): Evento => ({ + id, + tipo, + titolo: id, + luogo: "", + data, + ora: "21:00", + note: "", + convocati: [], + campionato: true, + casa: true, + pagelleChiuse: false, +}); + +// --- dataOggi ---------------------------------------------------------------- +assert.match(dataOggi(), /^\d{4}-\d{2}-\d{2}$/); +assert.equal(dataOggi(), new Date().toLocaleDateString("sv-SE"), "data locale, non UTC"); + +// --- partitaDiOggi ----------------------------------------------------------- +const eventi = [ + evento("a1", "2026-09-01", "allenamento"), + evento("p1", "2026-09-01", "partita"), + evento("p2", "2026-09-02", "partita"), +]; +assert.equal(partitaDiOggi(eventi, "2026-09-01")?.id, "p1", "l'allenamento non si scoutizza"); +assert.equal(partitaDiOggi(eventi, "2026-09-03"), null, "nessuna partita oggi"); +assert.equal(partitaDiOggi([], "2026-09-01"), null); + +// --- sessioneScaduta: libera il tavolo dopo SCADENZA_MINUTI ------------------ +const sessione = (minutiFa: number): SessioneScout => ({ + evento_id: "p1", + giocatore_id: "g1", + giocatore_nome: "Tizio", + aggiornato_il: new Date(Date.now() - minutiFa * 60_000).toISOString(), +}); + +assert.equal(sessioneScaduta(null), true, "nessuna sessione = tavolo libero"); +assert.equal(sessioneScaduta(sessione(0)), false, "appena aggiornata"); +assert.equal(sessioneScaduta(sessione(SCADENZA_MINUTI - 1)), false, "dentro la finestra"); +assert.equal(sessioneScaduta(sessione(SCADENZA_MINUTI + 1)), true, "oltre la finestra"); +assert.equal( + sessioneScaduta({ ...sessione(0), aggiornato_il: "data-non-valida" }), + true, + "timestamp illeggibile: meglio liberare la sessione che bloccarla", +); + +console.log("scout-live: ok");