// Open-Meteo (keyless, works from the VPS): kickoff-hour weather per venue. // Venues are pre-geocoded at build time into public/data/venues.json — // includes elevation (Mexico City 2,240m), which is its own story. import { cachedFetch } from './fetcher'; import { loadStatic } from '../preview'; const SOURCE = 'open-meteo'; interface VenuesFile { venues: Record; } let _venues: VenuesFile | null = null; const venues = (): VenuesFile => (_venues ??= loadStatic('venues.json')); export interface MatchWeather { v: 1; fetchedAt: number; tempC: number | null; precipProbPct: number | null; windKmh: number | null; elevationM: number; tz: string; } interface Forecast { hourly?: { time?: string[]; temperature_2m?: number[]; precipitation_probability?: number[]; wind_speed_10m?: number[] }; } /** Kickoff-hour forecast for a fixture's venue (forecast horizon ≈16 days). */ export async function fetchMatchWeather(venue: string, kickoffIso: string): Promise { const v = venues().venues[venue]; if (!v) return null; const url = `https://api.open-meteo.com/v1/forecast?latitude=${v.lat}&longitude=${v.lon}` + `&hourly=temperature_2m,precipitation_probability,wind_speed_10m&forecast_days=16&timezone=UTC`; const f = await cachedFetch(SOURCE, `weather:${venue}`, url, { ttl: 3 * 60 * 60_000, minInterval: 1_000 }); const hours = f.hourly?.time ?? []; const wantHour = kickoffIso.slice(0, 13); // YYYY-MM-DDTHH const idx = hours.findIndex((t) => t.startsWith(wantHour)); if (idx < 0) return null; return { v: 1, fetchedAt: Date.now(), tempC: f.hourly?.temperature_2m?.[idx] ?? null, precipProbPct: f.hourly?.precipitation_probability?.[idx] ?? null, windKmh: f.hourly?.wind_speed_10m?.[idx] ?? null, elevationM: v.elevation, tz: v.tz, }; }