From 7ef7eef9635539c63078be96ac3f51fe89424038 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Mon, 7 Sep 2026 09:47:02 +0200 Subject: [PATCH] Calcola "oggi" nel fuso di Roma ovunque, invece di UTC o del fuso del processo. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit serieConsecutiva()/serieConferme() calcolavano oggi con new Date().toISOString() (sempre UTC), mentre contaPresenzeGiocatore() usava dataOggi() con i getter locali di Date (corretti solo se il processo gira già in fuso italiano — falso su un server SSR in UTC). Le due statistiche potevano non essere d'accordo su cosa fosse "oggi" nelle prime ore della giornata italiana. dataOggi() ora usa Intl.DateTimeFormat con timeZone: "Europe/Rome": il cambio ora legale/solare lo gestisce il database IANA dei fusi, non un offset scritto a mano. Le funzioni di serie in presenze.ts usano lo stesso dataOggi() invece di un oggiIso() locale, così tutte le statistiche restano coerenti fra loro. Aggiunti test che dimostrano il fix con istanti reali a cavallo di mezzanotte sia in CET che in CEST, per provare che lo scarto segue davvero il fuso e non un offset fisso. Co-Authored-By: Claude Sonnet 5 --- docs/modules/serie-presenze.md | 10 ++++++++-- src/lib/presenze.ts | 8 ++------ src/lib/scout-live.ts | 12 +++++++++--- test/unit/scout-live.test.ts | 18 ++++++++++++++++-- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/docs/modules/serie-presenze.md b/docs/modules/serie-presenze.md index 8d06285..4a90ae9 100644 --- a/docs/modules/serie-presenze.md +++ b/docs/modules/serie-presenze.md @@ -316,8 +316,14 @@ nell'ordine in cui arrivano dalla query (`.order("data")`), quindi non determini loro. Irrilevante finché un buco e una presenza nello stesso giorno danno lo stesso risultato finale, ma va sistemato se un giorno serve l'ordine esatto. -**Il fuso è quello del client.** `oggi` nasce da `new Date().toISOString()`, cioè UTC: nelle -prime ore della giornata italiana un evento di oggi può risultare "non ancora passato". +**`oggi` è sempre in fuso Italia.** `dataOggi()` (`src/lib/scout-live.ts`) usa +`Intl.DateTimeFormat` con `timeZone: "Europe/Rome"`, non i getter locali di `Date` né +`toISOString()`: il cambio ora legale/solare lo gestisce il database IANA dei fusi, non un +offset scritto a mano. È lo stesso `oggi` di `serieConsecutiva()`, `serieConferme()` e del +conteggio presenze — prima `serieConsecutiva()`/`serieConferme()` calcolavano `oggi` con +`toISOString()` (sempre UTC) mentre il conteggio presenze usava i getter locali di `Date` +(corretti solo se il processo gira già in fuso italiano): nelle prime ore della giornata +italiana potevano non essere d'accordo su cosa fosse "oggi". --- diff --git a/src/lib/presenze.ts b/src/lib/presenze.ts index e24d4c8..6ca3a74 100644 --- a/src/lib/presenze.ts +++ b/src/lib/presenze.ts @@ -77,7 +77,7 @@ export function serieConsecutiva( eventi: Evento[], presenze: MappaPresenze, tipo?: "partita" | "allenamento", - oggi: string = oggiIso(), + oggi: string = dataOggi(), ): number { return serieSu( giocatoreId, @@ -102,7 +102,7 @@ export function serieConferme( giocatoreId: string, eventi: Evento[], tempi: MappaTempiRisposta, - oggi: string = oggiIso(), + oggi: string = dataOggi(), ): number { return serieSu( giocatoreId, @@ -116,10 +116,6 @@ export function serieConferme( ); } -function oggiIso() { - return new Date().toISOString().slice(0, 10); -} - /** Scorre gli eventi già passati in ordine di data applicando la regola delle serie. */ function serieSu( giocatoreId: string, diff --git a/src/lib/scout-live.ts b/src/lib/scout-live.ts index b3b7fd4..2e739db 100644 --- a/src/lib/scout-live.ts +++ b/src/lib/scout-live.ts @@ -6,9 +6,15 @@ import { useEventi, type Evento } from "./eventi"; /** Minuti dopo i quali una sessione scout inattiva viene considerata libera. */ export const SCADENZA_MINUTI = 5; -export function dataOggi(): string { - const d = new Date(); - return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; +/** + * Data di oggi in Italia (`Europe/Rome`, con cambio ora legale/solare gestito dal database + * IANA dei fusi orari). Non i getter locali di `Date` — quelli seguono il fuso del processo + * che esegue il codice: sul client di chi è in Italia coincide, ma lato server (SSR, dev + * container) è spesso UTC, e `toISOString()` lo è sempre. Sbagliare qui sposta la mezzanotte: + * nella prima ora o due della giornata italiana, eventi già passati risulterebbero "di domani". + */ +export function dataOggi(adesso: Date = new Date()): string { + return new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Rome" }).format(adesso); } /** La partita in programma oggi, se c'è. */ diff --git a/test/unit/scout-live.test.ts b/test/unit/scout-live.test.ts index 6b31ed5..3908d10 100644 --- a/test/unit/scout-live.test.ts +++ b/test/unit/scout-live.test.ts @@ -23,9 +23,23 @@ const evento = (id: string, data: string, tipo: Evento["tipo"]): Evento => ({ pagelleChiuse: false, }); -// --- dataOggi ---------------------------------------------------------------- +// --- dataOggi ------------------------------------------------------------------ assert.match(dataOggi(), /^\d{4}-\d{2}-\d{2}$/); -assert.equal(dataOggi(), new Date().toLocaleDateString("sv-SE"), "data locale, non UTC"); +// CET (gennaio, UTC+1): dopo le 23:00 UTC del 15, a Roma è già il 16. L'ISO in UTC +// direbbe ancora 15: se `dataOggi()` tornasse a farlo, questo test lo scoprirebbe. +assert.equal( + dataOggi(new Date("2026-01-15T23:30:00Z")), + "2026-01-16", + "CET: mezzanotte italiana precede quella UTC di un'ora", +); +// CEST (luglio, UTC+2): lo scarto raddoppia, la mezzanotte italiana anticipa quella UTC +// di due ore. Se il calcolo usasse un offset fisso invece del fuso Europe/Rome, questo +// secondo caso lo tradirebbe anche se il primo passasse per caso. +assert.equal( + dataOggi(new Date("2026-07-15T22:30:00Z")), + "2026-07-16", + "CEST: il cambio ora legale porta lo scarto a due ore, non resta fisso a uno", +); // --- partitaDiOggi ----------------------------------------------------------- const eventi = [