Fix daily-tier ordering: filter by cutoff date, not exact-match skip list

Met Office's daily series can include a stray leftover entry for
today/yesterday that the old exact-match skip set (today, +1, +2)
didn't catch. It sorted to the front of the 4-day window and showed
up as an extra day ahead of the real day+3..+6 (live symptom: kiosk
bottom row showing Sun, Thu, Fri, Sat instead of Thu, Fri, Sat, Sun).

Now filters on "date key > cutoff" (day+2) so any stray day before
the window is excluded regardless of why it appeared, and explicitly
sorts before slicing so the window is chronological even if the
provider's array isn't. Applied to both the Met Office and Open-Meteo
daily parsers for consistency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-31 11:23:14 +00:00
parent c19a0389e6
commit d094e78885

View file

@ -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'
}