diff --git a/backend/src/lib/weather-fetch.js b/backend/src/lib/weather-fetch.js index 94cab47..349e522 100644 --- a/backend/src/lib/weather-fetch.js +++ b/backend/src/lib/weather-fetch.js @@ -122,9 +122,20 @@ async function fetchMetOfficeThreeHourly(lat, lon, apiKey, dayKeys) { // response is the only one with sunrise/sunset fields, so the two travel together; // if this tier falls back to Open-Meteo, astro falls back with it (see // openMeteoAstroPoints below). -async function fetchMetOfficeDaily(lat, lon, apiKey, skipKeys, astroKeys) { +// +// Filters by "date key > cutoffKey" (day+2) rather than excluding an exact set of +// keys — Met Office's daily series has been observed to include a stray leftover +// entry for today/yesterday that an exact-match skip list doesn't catch, which +// then sorts to the front of the window and shows up as an extra day ahead of the +// real day+3..+6 (seen live 2026-08-31: a stray Sunday entry displayed before +// Thu/Fri/Sat). Explicitly sorting before slicing guards the same window against +// the provider not returning entries in chronological order at all. +async function fetchMetOfficeDaily(lat, lon, apiKey, cutoffKey, astroKeys) { const series = await fetchMetOfficeTimestep('daily', lat, lon, apiKey) - const dayRows = series.filter(p => !skipKeys.has(londonDateKey(p.time))).slice(0, 4) + const dayRows = series + .filter(p => londonDateKey(p.time) > cutoffKey) + .sort((a, b) => a.time.localeCompare(b.time)) + .slice(0, 4) const dailyPoints = assertParsed(dayRows.length, dayRows .map(p => ({ resolution: 'daily', @@ -211,9 +222,10 @@ function openMeteoThreeHourlyPoints(hourlyRows, dayKeys) { source: 'open_meteo', })) } -function openMeteoDailyPoints(dailyRows, skipKeys) { +function openMeteoDailyPoints(dailyRows, cutoffKey) { return dailyRows - .filter(r => !skipKeys.has(r.dateKey)) + .filter(r => r.dateKey > cutoffKey) + .sort((a, b) => a.dateKey.localeCompare(b.dateKey)) .slice(0, 4) .map(r => ({ resolution: 'daily', @@ -321,7 +333,10 @@ export async function refreshForecast(log = console) { const today = londonDateKey(new Date().toISOString()) const dayKeys = [londonDateKeyPlusDays(1), londonDateKeyPlusDays(2)] - const skipKeys = new Set([today, ...dayKeys]) + // The 'daily' tier (days 4-7) keeps anything strictly after this date key — + // see fetchMetOfficeDaily's comment for why an exact-match skip list isn't + // used here. + const cutoffKey = dayKeys[dayKeys.length - 1] const astroKeys = [today, ...dayKeys] const metOffice = await getIntegration('met_office') @@ -337,7 +352,7 @@ export async function refreshForecast(log = console) { const [hourlyR, threeHourlyR, dailyR] = await Promise.allSettled([ fetchWithKeys(key => fetchMetOfficeHourly(lat, lon, key), keys), fetchWithKeys(key => fetchMetOfficeThreeHourly(lat, lon, key, dayKeys), keys), - fetchWithKeys(key => fetchMetOfficeDaily(lat, lon, key, skipKeys, astroKeys), keys), + fetchWithKeys(key => fetchMetOfficeDaily(lat, lon, key, cutoffKey, astroKeys), keys), ]) if (hourlyR.status === 'fulfilled') { @@ -376,7 +391,7 @@ export async function refreshForecast(log = console) { tierSource.threeHourly = 'open_meteo' } if (!tierSource.daily) { - dailyPoints = openMeteoDailyPoints(dailyRows, skipKeys) + dailyPoints = openMeteoDailyPoints(dailyRows, cutoffKey) astroPoints = openMeteoAstroPoints(dailyRows, astroKeys) tierSource.daily = 'open_meteo' }