fix(climato): combler le lag de publication via SYNOP 48h
All checks were successful
Deploy info-canicule / deploy (push) Successful in 1m33s

Fetcher 48h d'obs horaires (au lieu de 24h) pour avoir le jour J-1 complet.
Dériver TX/TN journaliers (max/min des températures horaires) et les injecter
dans les fenêtres 7j/30j/1an pour les jours absents de la climato CSV.
Le tab "24h" du graphique reste filtré sur les 24 dernières heures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Florian 2026-05-28 12:47:24 +02:00
parent a1c6002756
commit e247005d34

View file

@ -30,7 +30,7 @@ const [snapshotR, climatoR, hourlyR] = drom
: ([
getVigilanceSnapshot(),
getClimatoForDepartement(dept.code),
getHourlyForDepartement(dept.code, 24),
getHourlyForDepartement(dept.code, 48),
] as const);
const settled = await Promise.allSettled([snapshotR, climatoR, hourlyR]);
@ -58,13 +58,35 @@ let normaleHourly: { tx: number | null; tn: number | null } | null = null;
// Source data has a ~1-4 day publication lag; the chart must still end at today.
const _todayParis = new Intl.DateTimeFormat('sv', { timeZone: 'Europe/Paris' }).format(new Date());
const _climaByDate = new Map((climato?.days ?? []).map((d) => [d.date, d]));
// Derive daily TX/TN from the 48h SYNOP window to fill the climato publication gap (typically 1-2 days).
// TX = max hourly t, TN = min hourly t — approximation acceptable for gap-filling.
const _synopDaily = new Map<string, { tx: number | null; tn: number | null }>();
if (hourly) {
const _byDate = new Map<string, number[]>();
for (const obs of hourly.observations) {
if (obs.t === null) continue;
const d = new Intl.DateTimeFormat('sv', { timeZone: 'Europe/Paris' }).format(new Date(obs.time));
if (!_byDate.has(d)) _byDate.set(d, []);
_byDate.get(d)!.push(obs.t);
}
for (const [date, temps] of _byDate) {
_synopDaily.set(date, { tx: +Math.max(...temps).toFixed(1), tn: +Math.min(...temps).toFixed(1) });
}
}
function _buildRange(n: number) {
const result = [];
for (let i = n - 1; i >= 0; i--) {
const d = new Date(_todayParis + 'T12:00:00Z'); // noon UTC to avoid DST drift
d.setUTCDate(d.getUTCDate() - i);
const dateStr = new Intl.DateTimeFormat('sv', { timeZone: 'Europe/Paris' }).format(d);
result.push(_climaByDate.get(dateStr) ?? { date: dateStr, tn: null, tx: null, tm: null, rr: null, stations: 0 });
if (_climaByDate.has(dateStr)) {
result.push(_climaByDate.get(dateStr)!);
} else {
const synop = _synopDaily.get(dateStr);
result.push({ date: dateStr, tn: synop?.tn ?? null, tx: synop?.tx ?? null, tm: null, rr: null, stations: 0 });
}
}
return result;
}
@ -88,6 +110,11 @@ if (!drom) {
if (normaleHourly) normaleHourly = { tx: normaleHourly.tx, tn: normaleHourly.tn };
}
// Pass only the last 24h slice to the chart (we fetched 48h for synop gap-filling above).
const hourlyFor24h = hourly
? { ...hourly, observations: hourly.observations.filter((o) => new Date(o.time).getTime() >= Date.now() - 24 * 3600 * 1000) }
: null;
const stationLabel = hourly ? `${hourly.stationName} (${hourly.distKm} km)` : null;
const ech = snapshot ? currentEcheance(snapshot) : 'J';
@ -361,7 +388,7 @@ const topPhen = topAlert ? PHENOMENA[topAlert.phenomenonId] : null;
<div class="ic-card" style="padding: clamp(16px, 3vw, 28px);">
<h2 style="font-size: clamp(1.2rem, 1rem + 0.8vw, 1.6rem); margin-bottom: 20px;">Températures récentes</h2>
<TemperatureChartInteractive
hourly={hourly}
hourly={hourlyFor24h}
days7={last7}
days30={last30}
days365={last365}