Segnala in modo visibile se il portale CSI cambia formato
Le vittorie in campionato dipendono dal JSON delle partite (non dalla classifica HTML, come erroneamente documentato prima). Se quel formato cambia, il parsing fallisce in silenzio: nessun errore, nessuna notifica, le vittorie restano ferme a 0% finché qualcuno non se ne accorge per caso. Aggiunta partiteFormatoSospetto() in csi-core.ts: confronta gli eventi grezzi con il risultato del parsing per distinguere un vero "formato cambiato" da un legittimo "nessuna gara ancora in programma". La route logga l'errore server e il flag arriva fino a /classifica, dove sostituisce la riga "Dati CSI aggiornati alle..." con un badge discreto color warning - visibile a chi apre la pagina, senza notifiche invasive. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,34 @@ useCsi() → src/lib/csi.ts (React Query, staleTime 6h)
|
||||
istanze. Sufficiente per una squadra; se serve di più, spostare i dati in una tabella
|
||||
Supabase riempita da un job cron (stesso pattern di `promemoria-palloni`).
|
||||
4. **I risultati includono anche la Coppa**, non solo il girone di campionato.
|
||||
5. **Le partite si leggono da JSON, con parsing fragile su campi testuali.** `result` e
|
||||
`partials` in `getEventsByTeamId.php` sono stringhe libere tipo `"3-1"`, lette con
|
||||
un'espressione regolare (`punteggio()`/`parziali()` in `csi-core.ts`). Se il portale CSI
|
||||
cambiasse formato (es. `"3:1"`, o un punteggio come oggetto invece che stringa), la regex
|
||||
non troverebbe corrispondenza e la partita risulterebbe "non ancora giocata"
|
||||
(`setNostri`/`setLoro` a `null`) — silenziosamente, senza errori. Se invece la risposta
|
||||
cambiasse forma radicalmente (non più un array), `partiteDaEventi()` torna `[]`.
|
||||
**Conseguenza sugli obiettivi di squadra**: le "vittorie in campionato" (`obiettivi.ts`,
|
||||
obiettivi o3/o4/o5) dipendono da `partiteGiocate(csi.partite)` — se il parsing delle partite
|
||||
si rompe così, questi tre obiettivi restano bloccati a 0% anche a fronte di vittorie reali.
|
||||
**Il fallback della route non se ne accorgerebbe da solo**: `/api/public/csi` lancia un
|
||||
errore solo se *sia* la classifica *sia* le partite sono vuote insieme
|
||||
(`classifica.length === 0 && partite.length === 0`); se si rompe solo il parsing delle
|
||||
partite mentre la classifica HTML continua a funzionare, la route risponde comunque `200`
|
||||
con `partite: []`. Per questo `leggiCsi()` confronta il JSON grezzo con il risultato di
|
||||
`partiteDaEventi()` tramite `partiteFormatoSospetto()` (`csi-core.ts`): se ci sono eventi
|
||||
grezzi ma nessuno è stato riconosciuto come nostra partita, logga un `console.error` —
|
||||
distingue così un vero "formato cambiato" da un legittimo "nessuna gara ancora in
|
||||
programma" (dove gli eventi grezzi stessi sono vuoti). Il flag `formatoSospetto` viaggia
|
||||
anche nella risposta JSON (`DatiCsi.formatoSospetto`) fino a `/classifica`
|
||||
(`src/routes/classifica.tsx`), dove mostra un badge discreto ("Il portale CSI potrebbe aver
|
||||
cambiato formato: dati da verificare.") al posto della normale riga "Dati CSI aggiornati
|
||||
alle...": un log server passa inosservato per settimane, un badge visibile a chi apre la
|
||||
pagina campionato molto meno. Il fix, quando succede, è isolato a
|
||||
`partiteDaEventi()`/`punteggio()`/`parziali()` in `csi-core.ts` (gli endpoint stessi
|
||||
cambiano solo se cambia il dominio o serve autenticazione, nel qual caso va toccata anche
|
||||
`src/routes/api/public/csi.ts`); va poi aggiornato anche `test/unit/csi-core.test.ts` con
|
||||
fixture nel nuovo formato.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -43,8 +43,10 @@ smart (`notifiche-smart.ts`).
|
||||
|
||||
## Limiti noti
|
||||
|
||||
- Le vittorie di campionato dipendono dal parsing HTML del portale CSI: se quel parsing si
|
||||
rompe, questi tre obiettivi restano a 0% anche a fronte di vittorie reali.
|
||||
- Le vittorie di campionato dipendono dal **JSON** delle partite del portale CSI (non dalla
|
||||
classifica HTML): se quel parsing si rompe, questi tre obiettivi restano a 0% anche a fronte
|
||||
di vittorie reali, **senza segnalazione d'errore** — vedi il dettaglio in
|
||||
[Collegamento CSI § Limiti noti](collegamento-csi.md#limiti-noti).
|
||||
- I target (250 presenze, 200 pagelle, ecc.) sono costanti fisse, da rivedere manualmente a
|
||||
ogni stagione — con l'eccezione di "Continuità di squadra" (vedi sotto), il cui target ha un
|
||||
significato specifico e non va scalato come gli altri.
|
||||
|
||||
@@ -39,6 +39,8 @@ export type DatiCsi = {
|
||||
partite: PartitaCsi[];
|
||||
girone: string;
|
||||
aggiornato: string;
|
||||
/** true se il formato delle partite sembra cambiato (vedi `partiteFormatoSospetto`). */
|
||||
formatoSospetto: boolean;
|
||||
};
|
||||
|
||||
/** "C.R.A.P. Volley" e "CRAP Volley" devono confrontarsi uguali. */
|
||||
@@ -162,6 +164,20 @@ export function partiteGiocate(partite: PartitaCsi[]): PartitaCsi[] {
|
||||
return partite.filter((p) => p.setNostri !== null && p.setLoro !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* True se il formato di `getEventsByTeamId.php` sembra cambiato: `partiteDaEventi()` fallisce
|
||||
* in modo silenzioso (nessun array o campi non riconosciuti), quindi un array vuoto da solo non
|
||||
* distingue "il portale CSI ha cambiato formato" da "la squadra non ha ancora gare in
|
||||
* programma". Qui invece si confronta con la risposta grezza: se contiene eventi ma nessuno è
|
||||
* stato riconosciuto come nostra partita, è quasi certamente un problema di parsing, non una
|
||||
* stagione senza gare. Usata da `/api/public/csi` per loggare il caso invece di lasciarlo
|
||||
* silenzioso — vedi "Limiti noti" in docs/modules/collegamento-csi.md.
|
||||
*/
|
||||
export function partiteFormatoSospetto(eventiGrezzi: unknown, partite: PartitaCsi[]): boolean {
|
||||
if (!Array.isArray(eventiGrezzi)) return true;
|
||||
return eventiGrezzi.length > 0 && partite.length === 0;
|
||||
}
|
||||
|
||||
/** Converte una gara CSI già giocata nella forma comune usata nelle liste risultati. */
|
||||
export function matchDaPartitaCsi(p: PartitaCsi) {
|
||||
return {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CSI_GIRONE,
|
||||
parseClassifica,
|
||||
partiteDaEventi,
|
||||
partiteFormatoSospetto,
|
||||
urlClassifica,
|
||||
urlPartite,
|
||||
type DatiCsi,
|
||||
@@ -29,11 +30,29 @@ async function scarica(url: string): Promise<string> {
|
||||
async function leggiCsi(): Promise<DatiCsi> {
|
||||
const [html, json] = await Promise.all([scarica(urlClassifica()), scarica(urlPartite())]);
|
||||
const classifica = parseClassifica(html);
|
||||
const partite = partiteDaEventi(JSON.parse(json));
|
||||
const eventiGrezzi = JSON.parse(json);
|
||||
const partite = partiteDaEventi(eventiGrezzi);
|
||||
if (classifica.length === 0 && partite.length === 0) {
|
||||
throw new Error("CSI: risposta senza classifica né partite");
|
||||
}
|
||||
return { classifica, partite, girone: CSI_GIRONE, aggiornato: new Date().toISOString() };
|
||||
// La classifica basta a evitare l'errore sopra, ma se solo le partite si rompono (formato di
|
||||
// getEventsByTeamId.php cambiato) la route tornerebbe comunque 200 senza che nessuno se ne
|
||||
// accorga: le vittorie degli obiettivi di squadra resterebbero ferme a 0% in silenzio. Oltre
|
||||
// al log server, il flag arriva fino a `/classifica` (badge discreto) perché qualcuno se ne
|
||||
// accorga anche senza guardare i log.
|
||||
const formatoSospetto = partiteFormatoSospetto(eventiGrezzi, partite);
|
||||
if (formatoSospetto) {
|
||||
console.error(
|
||||
"csi: il formato di getEventsByTeamId.php sembra cambiato, nessuna partita riconosciuta",
|
||||
);
|
||||
}
|
||||
return {
|
||||
classifica,
|
||||
partite,
|
||||
girone: CSI_GIRONE,
|
||||
aggiornato: new Date().toISOString(),
|
||||
formatoSospetto,
|
||||
};
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/api/public/csi")({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { ChevronRight, RefreshCw } from "lucide-react";
|
||||
import { AlertTriangle, ChevronRight, RefreshCw } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatData } from "@/lib/crapp-data";
|
||||
import { PageHeader } from "@/components/crapp/ui-bits";
|
||||
@@ -85,11 +85,24 @@ function Classifica() {
|
||||
const contenutoClassifica = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<div className="mb-3 flex items-center gap-2 rounded-2xl bg-secondary px-3 py-2 text-xs text-muted-foreground">
|
||||
<RefreshCw className="h-3.5 w-3.5 text-accent" />
|
||||
{csi
|
||||
? `Dati CSI aggiornati ${formatAggiornamento(csi.aggiornato)}`
|
||||
: "Dati CSI in arrivo"}
|
||||
<div
|
||||
className={cn(
|
||||
"mb-3 flex items-center gap-2 rounded-2xl px-3 py-2 text-xs",
|
||||
csi?.formatoSospetto
|
||||
? "bg-warning/15 text-warning"
|
||||
: "bg-secondary text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{csi?.formatoSospetto ? (
|
||||
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-warning" aria-hidden />
|
||||
) : (
|
||||
<RefreshCw className="h-3.5 w-3.5 shrink-0 text-accent" aria-hidden />
|
||||
)}
|
||||
{csi?.formatoSospetto
|
||||
? "Il portale CSI potrebbe aver cambiato formato: dati da verificare."
|
||||
: csi
|
||||
? `Dati CSI aggiornati ${formatAggiornamento(csi.aggiornato)}`
|
||||
: "Dati CSI in arrivo"}
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-3xl bg-card shadow-card">
|
||||
<div className="grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] gap-2 border-b border-border px-3 py-2 text-xs font-bold uppercase text-muted-foreground">
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isNostraSquadra,
|
||||
parseClassifica,
|
||||
partiteDaEventi,
|
||||
partiteFormatoSospetto,
|
||||
partiteGiocate,
|
||||
urlClassifica,
|
||||
urlPartite,
|
||||
@@ -96,6 +97,34 @@ assert.deepEqual(trasferta.parziali[0], [21, 25], "anche i parziali");
|
||||
assert.equal(partiteGiocate(partite).length, 2);
|
||||
assert.deepEqual(partiteDaEventi("non è un array"), [], "risposta inattesa: nessun crash");
|
||||
|
||||
// --- partiteFormatoSospetto: distingue "nessuna gara ancora" da "formato rotto" -------------
|
||||
assert.equal(
|
||||
partiteFormatoSospetto(eventi, partite),
|
||||
false,
|
||||
"eventi grezzi presenti e riconosciuti: nessun sospetto",
|
||||
);
|
||||
assert.equal(
|
||||
partiteFormatoSospetto([], []),
|
||||
false,
|
||||
"nessun evento grezzo: probabilmente solo una stagione senza gare, non un formato rotto",
|
||||
);
|
||||
assert.equal(
|
||||
partiteFormatoSospetto("non è un array", []),
|
||||
true,
|
||||
"la risposta non è più un array: il formato è cambiato",
|
||||
);
|
||||
const eventiSenzaCampiRiconosciuti = [
|
||||
{ id: 1, quando: "2025-11-12", squadraCasa: "C.R.A.P. Volley", squadraOspite: "AMCM" },
|
||||
];
|
||||
assert.equal(
|
||||
partiteFormatoSospetto(
|
||||
eventiSenzaCampiRiconosciuti,
|
||||
partiteDaEventi(eventiSenzaCampiRiconosciuti),
|
||||
),
|
||||
true,
|
||||
"eventi presenti ma con campi rinominati: nessuno riconosciuto, sospetto fondato",
|
||||
);
|
||||
|
||||
if (process.env["CSI_LIVE"]) {
|
||||
const [html, json] = await Promise.all([
|
||||
fetch(urlClassifica()).then((r) => r.text()),
|
||||
|
||||
Reference in New Issue
Block a user