Aggiunge il dettaglio esteso di partita: formazioni e scontri diretti dal CSI

In Campionato > Storico partite ogni gara diventa cliccabile: verso /partita/$id
se collegata a un evento CrAPP, verso la nuova /partita-csi/$id altrimenti (nessuna
creazione automatica di eventi dal calendario CSI, quindi molte gare ne sono prive).

Il dettaglio aggiunge formazioni (titolari/panchina/staff), storico scontri diretti
e probabilità di vittoria calcolata dal CSI, letti on-demand da tre nuovi endpoint
per-partita (match-main/players/stats.php) dietro /api/public/csi-partita/$id, con
cache server per-partita separata da quella di /api/public/csi — è un dato aperto a
richiesta, non precaricato per tutti come classifica e partite.

Include anche logo squadra e metadati leggeri (girone, n° gara, arbitro, link al
referto ufficiale) già presenti nel JSON esistente, a costo zero.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 12:07:03 +02:00
co-authored by Claude Sonnet 5
parent c628997934
commit 5569d08b59
12 changed files with 1207 additions and 38 deletions
+45 -1
View File
@@ -5,7 +5,12 @@
*/
import assert from "node:assert/strict";
import { giocatori } from "@/lib/crapp-data";
import { isNostraSquadra, partiteGiocate, type DatiCsi } from "@/lib/csi-core";
import {
isNostraSquadra,
partiteGiocate,
type DatiCsi,
type DettaglioPartitaCsi,
} from "@/lib/csi-core";
import { obiettiviSquadra } from "@/lib/obiettivi";
import { avviaServer, haSupabase, json } from "../helpers/server";
import { prova, riepilogo, salta } from "../helpers/prova";
@@ -119,6 +124,45 @@ try {
assert.ok(Date.now() - t0 < 2_000, "risposta immediata");
});
await provaCsi(
"GET /api/public/csi-partita/$id restituisce formazioni e precedenti",
async () => {
const primaGiocata = partiteGiocate(csi?.partite ?? [])[0];
assert.ok(primaGiocata, "serve almeno una gara giocata per testare il dettaglio");
const res = await fetch(url(`/api/public/csi-partita/${primaGiocata.id}`));
assert.equal(res.status, 200);
const dettaglio = (await json(res)) as DettaglioPartitaCsi;
// formazioni/precedenti possono essere null se il portale non li pubblica per questa
// gara (es. referto non compilato): qui si verifica solo che, quando presenti, siano
// coerenti — non che ci siano sempre, vedi parseFormazioni()/parsePrecedenti().
if (dettaglio.formazioni) {
assert.ok(isNostraSquadra(dettaglio.formazioni.noi.squadra));
assert.ok(dettaglio.formazioni.noi.titolari.length > 0, "almeno un titolare");
}
if (dettaglio.precedenti) {
assert.ok(dettaglio.precedenti.totale >= 0);
}
const t0 = Date.now();
const secondo = await fetch(url(`/api/public/csi-partita/${primaGiocata.id}`));
assert.equal(secondo.status, 200);
assert.ok(Date.now() - t0 < 2_000, "seconda chiamata dalla cache: risposta immediata");
},
);
await provaCsi(
"GET /api/public/csi-partita/$id con un id davvero inesistente risponde 503, non rompe",
async () => {
// Un match_id inventato fa rispondere 500 a match-stats.php lato CSI (verificato a
// mano con curl): non è "gara senza formazioni ancora pubblicate" (quello risponde
// 200 con markup vuoto, gestito da parseFormazioni()/parsePrecedenti() → null), è un
// vero errore a monte. La route lo tratta come /api/public/csi tratta un CSI giù:
// 503 pulito, non un crash o un 200 con dati inventati.
const res = await fetch(url("/api/public/csi-partita/999999999"));
assert.equal(res.status, 503);
},
);
// --- GET /api/public/push-config -------------------------------------------
await prova("GET /api/public/push-config espone solo la chiave pubblica", async () => {
const res = await fetch(url("/api/public/push-config"));