From 64afd2b5fd4c863adb92d10ae890b999a3c262b2 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Tue, 25 Aug 2026 14:06:31 +0000 Subject: [PATCH] Throw on wrong-field-name parse failures instead of silent zero-point success Spot is now confirmed subscribed to hourly/three-hourly too (settings' met_office test matrix), so those parsers are genuinely exercised for the first time -- previously a wrong field name would silently produce an empty points array while still reporting the tier as a 'met_office' success. assertParsed() throws when raw entries came back but none parsed a valid temp_c, so a field-name mismatch now correctly falls back to Open-Meteo (and logs an error) instead of quietly showing an empty today strip. Co-Authored-By: Claude Sonnet 5 --- backend/src/lib/weather-fetch.js | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/backend/src/lib/weather-fetch.js b/backend/src/lib/weather-fetch.js index f6ecd56..94cab47 100644 --- a/backend/src/lib/weather-fetch.js +++ b/backend/src/lib/weather-fetch.js @@ -62,12 +62,26 @@ async function fetchMetOfficeTimestep(timestep, lat, lon, apiKey) { return series } +// Throws if the raw response had entries but every one of them failed to parse a +// valid temp_c — that's not "no data for this range" (which is a legitimate empty +// array, not an error), it's "the field names are wrong". Confirmed live 2026-08-25 +// that Spot is actually subscribed to all three endpoints (settings' met_office +// test route), so hourly/three-hourly parsing is now genuinely exercised for the +// first time — this turns a silent wrong-field-name bug (reported as "success" +// with zero points) into a real failure that falls back to Open-Meteo instead. +function assertParsed(rawCount, points, label) { + if (rawCount > 0 && points.length === 0) { + throw new Error(`Met Office ${label}: got ${rawCount} raw entries but parsed 0 valid points — check field names`) + } + return points +} + // Every hour the endpoint returns, not just today — the "tap a Coming up card" // hourly-detail modal needs this for tomorrow/day-after (and, where available, // days 4-7) too. async function fetchMetOfficeHourly(lat, lon, apiKey) { const series = await fetchMetOfficeTimestep('hourly', lat, lon, apiKey) - return series + const points = series .map(p => ({ resolution: 'hourly', ts: p.time, @@ -81,12 +95,13 @@ async function fetchMetOfficeHourly(lat, lon, apiKey) { source: 'met_office', })) .filter(p => typeof p.temp_c === 'number') + return assertParsed(series.length, points, 'hourly') } async function fetchMetOfficeThreeHourly(lat, lon, apiKey, dayKeys) { const series = await fetchMetOfficeTimestep('three-hourly', lat, lon, apiKey) - return series - .filter(p => dayKeys.includes(londonDateKey(p.time))) + const dayRows = series.filter(p => dayKeys.includes(londonDateKey(p.time))) + const points = dayRows .map(p => ({ resolution: '3hourly', ts: p.time, @@ -100,6 +115,7 @@ async function fetchMetOfficeThreeHourly(lat, lon, apiKey, dayKeys) { source: 'met_office', })) .filter(p => typeof p.temp_c === 'number') + return assertParsed(dayRows.length, points, 'three-hourly') } // Returns both the daily tier's points AND astro (sunrise/sunset) — the daily @@ -108,9 +124,8 @@ async function fetchMetOfficeThreeHourly(lat, lon, apiKey, dayKeys) { // openMeteoAstroPoints below). async function fetchMetOfficeDaily(lat, lon, apiKey, skipKeys, astroKeys) { const series = await fetchMetOfficeTimestep('daily', lat, lon, apiKey) - const dailyPoints = series - .filter(p => !skipKeys.has(londonDateKey(p.time))) - .slice(0, 4) + const dayRows = series.filter(p => !skipKeys.has(londonDateKey(p.time))).slice(0, 4) + const dailyPoints = assertParsed(dayRows.length, dayRows .map(p => ({ resolution: 'daily', ts: p.time, @@ -123,7 +138,7 @@ async function fetchMetOfficeDaily(lat, lon, apiKey, skipKeys, astroKeys) { icon: iconFromMetOffice(p.daySignificantWeatherCode), source: 'met_office', })) - .filter(p => typeof p.temp_c === 'number') + .filter(p => typeof p.temp_c === 'number'), 'daily') const astroPoints = series .filter(p => astroKeys.includes(londonDateKey(p.time))) .map(p => ({ date: londonDateKey(p.time), sunrise: p.sunrise ?? null, sunset: p.sunset ?? null }))