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 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-25 14:06:31 +00:00
parent 15a801b1f4
commit 64afd2b5fd

View file

@ -62,12 +62,26 @@ async function fetchMetOfficeTimestep(timestep, lat, lon, apiKey) {
return series 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" // 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, // hourly-detail modal needs this for tomorrow/day-after (and, where available,
// days 4-7) too. // days 4-7) too.
async function fetchMetOfficeHourly(lat, lon, apiKey) { async function fetchMetOfficeHourly(lat, lon, apiKey) {
const series = await fetchMetOfficeTimestep('hourly', lat, lon, apiKey) const series = await fetchMetOfficeTimestep('hourly', lat, lon, apiKey)
return series const points = series
.map(p => ({ .map(p => ({
resolution: 'hourly', resolution: 'hourly',
ts: p.time, ts: p.time,
@ -81,12 +95,13 @@ async function fetchMetOfficeHourly(lat, lon, apiKey) {
source: 'met_office', source: 'met_office',
})) }))
.filter(p => typeof p.temp_c === 'number') .filter(p => typeof p.temp_c === 'number')
return assertParsed(series.length, points, 'hourly')
} }
async function fetchMetOfficeThreeHourly(lat, lon, apiKey, dayKeys) { async function fetchMetOfficeThreeHourly(lat, lon, apiKey, dayKeys) {
const series = await fetchMetOfficeTimestep('three-hourly', lat, lon, apiKey) const series = await fetchMetOfficeTimestep('three-hourly', lat, lon, apiKey)
return series const dayRows = series.filter(p => dayKeys.includes(londonDateKey(p.time)))
.filter(p => dayKeys.includes(londonDateKey(p.time))) const points = dayRows
.map(p => ({ .map(p => ({
resolution: '3hourly', resolution: '3hourly',
ts: p.time, ts: p.time,
@ -100,6 +115,7 @@ async function fetchMetOfficeThreeHourly(lat, lon, apiKey, dayKeys) {
source: 'met_office', source: 'met_office',
})) }))
.filter(p => typeof p.temp_c === 'number') .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 // 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). // openMeteoAstroPoints below).
async function fetchMetOfficeDaily(lat, lon, apiKey, skipKeys, astroKeys) { async function fetchMetOfficeDaily(lat, lon, apiKey, skipKeys, astroKeys) {
const series = await fetchMetOfficeTimestep('daily', lat, lon, apiKey) const series = await fetchMetOfficeTimestep('daily', lat, lon, apiKey)
const dailyPoints = series const dayRows = series.filter(p => !skipKeys.has(londonDateKey(p.time))).slice(0, 4)
.filter(p => !skipKeys.has(londonDateKey(p.time))) const dailyPoints = assertParsed(dayRows.length, dayRows
.slice(0, 4)
.map(p => ({ .map(p => ({
resolution: 'daily', resolution: 'daily',
ts: p.time, ts: p.time,
@ -123,7 +138,7 @@ async function fetchMetOfficeDaily(lat, lon, apiKey, skipKeys, astroKeys) {
icon: iconFromMetOffice(p.daySignificantWeatherCode), icon: iconFromMetOffice(p.daySignificantWeatherCode),
source: 'met_office', source: 'met_office',
})) }))
.filter(p => typeof p.temp_c === 'number') .filter(p => typeof p.temp_c === 'number'), 'daily')
const astroPoints = series const astroPoints = series
.filter(p => astroKeys.includes(londonDateKey(p.time))) .filter(p => astroKeys.includes(londonDateKey(p.time)))
.map(p => ({ date: londonDateKey(p.time), sunrise: p.sunrise ?? null, sunset: p.sunset ?? null })) .map(p => ({ date: londonDateKey(p.time), sunrise: p.sunrise ?? null, sunset: p.sunset ?? null }))