diff --git a/src/pages/api/health.ts b/src/pages/api/health.ts index 4d7512d..3569d27 100644 --- a/src/pages/api/health.ts +++ b/src/pages/api/health.ts @@ -1,22 +1,40 @@ import type { APIRoute } from 'astro'; import { pingCache } from '../../lib/cache'; +import { getVigilanceSnapshot } from '../../lib/vigilance'; export const prerender = false; +// Endpoint d'usage interne : UptimeRobot (uptime) + cron HC.io freshness. +// Pas de CORS (pas destiné aux clients tiers). export const GET: APIRoute = async () => { const cacheOk = await pingCache(); + + // Best-effort : récupère le snapshot Vigilance pour exposer la fraîcheur. + // Si KO, on dégrade sans casser le health (l'uptime reste vert). + let vigilance: { productDatetime: string | null; ageSeconds: number | null } | null = null; + try { + const snap = await getVigilanceSnapshot(); + const pd = snap.productDatetime; + vigilance = { + productDatetime: pd, + ageSeconds: pd ? Math.floor((Date.now() - new Date(pd).getTime()) / 1000) : null, + }; + } catch { + vigilance = null; + } + const body = { status: cacheOk ? 'ok' : 'degraded', cache: cacheOk, time: new Date().toISOString(), + vigilance, }; + return new Response(JSON.stringify(body), { status: cacheOk ? 200 : 503, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', - // Pas de CORS : endpoint d'usage interne (UptimeRobot + cron HC.io freshness). - // Pas destiné aux clients tiers. }, }); }; diff --git a/src/pages/departement/[code].astro b/src/pages/departement/[code].astro index ec0c8d0..7ece3cd 100644 --- a/src/pages/departement/[code].astro +++ b/src/pages/departement/[code].astro @@ -23,50 +23,54 @@ if (!dept) { const drom = isDrom(dept.code); +// Fetch en parallèle des 3 sources externes (vigilance MF, climato data.gouv, hourly MF SYNOP). +// Cold-fetch climato = ~10s (4 MB de previous CSV.GZ), parallel ramène le total à max(individuel). +const [snapshotR, climatoR, hourlyR] = drom + ? [Promise.resolve(null), Promise.resolve(null), Promise.resolve(null)] as const + : ([ + getVigilanceSnapshot(), + getClimatoForDepartement(dept.code), + getHourlyForDepartement(dept.code, 24), + ] as const); + +const settled = await Promise.allSettled([snapshotR, climatoR, hourlyR]); let snapshot; let error: string | null = null; -if (!drom) { - try { - snapshot = await getVigilanceSnapshot(); - } catch (e) { - error = (e as Error).message; - } -} - let climato = null; -let anomaly = null; let hourly = null; + +if (settled[0].status === 'fulfilled') snapshot = settled[0].value as Awaited> | null; +else error = (settled[0].reason as Error).message; + +if (settled[1].status === 'fulfilled') climato = settled[1].value as Awaited> | null; +else console.warn('climato fetch failed for', dept.code, (settled[1].reason as Error).message); + +if (settled[2].status === 'fulfilled') hourly = settled[2].value as Awaited> | null; +else console.warn('hourly fetch failed for', dept.code, (settled[2].reason as Error).message); + +let anomaly = null; let normales7: Array<{ tx: number | null; tn: number | null }> = []; let normales30: Array<{ tx: number | null; tn: number | null }> = []; +let normales365: Array<{ tx: number | null; tn: number | null }> = []; let normaleHourly: { tx: number | null; tn: number | null } | null = null; -if (!drom) { - try { - climato = await getClimatoForDepartement(dept.code); - if (climato?.days?.length) { - anomaly = await computeAnomaly(dept.code, climato.days); - } - } catch (e) { - console.warn('climato fetch failed for', dept.code, (e as Error).message); - } - try { - hourly = await getHourlyForDepartement(dept.code, 24); - } catch (e) { - console.warn('hourly fetch failed for', dept.code, (e as Error).message); - } -} const last7 = climato?.days?.slice(-7) ?? []; const last30 = climato?.days?.slice(-30) ?? []; const last365 = climato?.days?.slice(-365) ?? []; -let normales365: Array<{ tx: number | null; tn: number | null }> = []; if (!drom) { - const series7 = await normalesForRange(dept.code, last7.map((d) => d.date)); - normales7 = series7.map((n) => ({ tx: n?.tx ?? null, tn: n?.tn ?? null })); - const series30 = await normalesForRange(dept.code, last30.map((d) => d.date)); - normales30 = series30.map((n) => ({ tx: n?.tx ?? null, tn: n?.tn ?? null })); - const series365 = await normalesForRange(dept.code, last365.map((d) => d.date)); - normales365 = series365.map((n) => ({ tx: n?.tx ?? null, tn: n?.tn ?? null })); + // Tous les lookups normales sont locaux (JSON statique) — pas besoin de paralléliser plus. + if (climato?.days?.length) { + anomaly = await computeAnomaly(dept.code, climato.days); + } + const [s7, s30, s365] = await Promise.all([ + normalesForRange(dept.code, last7.map((d) => d.date)), + normalesForRange(dept.code, last30.map((d) => d.date)), + normalesForRange(dept.code, last365.map((d) => d.date)), + ]); + normales7 = s7.map((n) => ({ tx: n?.tx ?? null, tn: n?.tn ?? null })); + normales30 = s30.map((n) => ({ tx: n?.tx ?? null, tn: n?.tn ?? null })); + normales365 = s365.map((n) => ({ tx: n?.tx ?? null, tn: n?.tn ?? null })); normaleHourly = await normaleForDate(dept.code, new Date()); if (normaleHourly) normaleHourly = { tx: normaleHourly.tx, tn: normaleHourly.tn }; }