517a86233a
Four new feeds, all verified against real tournament matches: - FotMob (unofficial, works from the VPS again): per-shot xG/xGOT with pitch coords, team xG, player ratings, POTM, momentum, attacking zones — trimmed payloads (~87KB/match) on a gentle 90s-live/30m-idle cadence. Capture-first: this source could lock again any day. - FIFA official v3: confirmed lineups with player IDs, typed XY event timelines (80 events for the opener), attendance + named referees, tactics — id-mapped via the calendar, hot while live. - Open-Meteo: kickoff-hour weather per venue from a build-time geocoded venues.json (with elevation — Azteca 2,240m); fetched in the enrich loop inside the 48h horizon. - DraftKings scorer props (first/last/anytime goalscorer per athlete) via ESPN's core propBets, insert-if-changed line history, athlete names resolved through a 30-day cache. Also fixes a real resilience gap found along the way: fixture results now persist in SQLite and restore at boot — previously a restart after ESPN's scoreboard window moved on would silently forget a match day (standings, model and scoreboard would all regress). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
// 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<string, { lat: number; lon: number; elevation: number; tz: string }>;
|
|
}
|
|
|
|
let _venues: VenuesFile | null = null;
|
|
const venues = (): VenuesFile => (_venues ??= loadStatic<VenuesFile>('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<MatchWeather | null> {
|
|
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<Forecast>(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,
|
|
};
|
|
}
|