Backfill NWS's missing pressure & UV from Open-Meteo (with source tooltip)

NWS doesn't publish pressure (its pressure series is empty) or a UV index.
When the selected source is NWS, merge current.pressure_msl, current.uv_index
and daily.uv_index_max from Open-Meteo and tag them via data._fallback so the
Current + Details views show a tooltip noting the value came from the
non-selected source (Open-Meteo) instead of showing 0 hPa / --.
This commit is contained in:
hermes-explorigin 2026-08-16 21:05:07 +00:00
parent ebe81b6bfa
commit c0ec45f170
5 changed files with 68 additions and 6 deletions

View File

@ -36,6 +36,8 @@
const unit = $derived(app.settings.units === 'imperial' ? '°F' : '°C') const unit = $derived(app.settings.units === 'imperial' ? '°F' : '°C')
const windUnit = $derived(app.settings.units === 'imperial' ? 'mph' : 'km/h') const windUnit = $derived(app.settings.units === 'imperial' ? 'mph' : 'km/h')
const fbPressure = $derived(!!data?._fallback?.pressure)
const fbUv = $derived(!!data?._fallback?.uv)
</script> </script>
{#if current} {#if current}
@ -69,12 +71,12 @@
<span class="metric-value">{Math.round(current.wind_speed_10m)} {windUnit}</span> <span class="metric-value">{Math.round(current.wind_speed_10m)} {windUnit}</span>
<span class="metric-sub">{windCompass(current.wind_direction_10m)}</span> <span class="metric-sub">{windCompass(current.wind_direction_10m)}</span>
</div> </div>
<div class="metric-card"> <div class="metric-card" title={fbPressure ? 'Pressure from Open-Meteo — NWS does not provide it' : undefined}>
<span class="metric-icon">🌡️</span> <span class="metric-icon">🌡️</span>
<span class="metric-label">Pressure</span> <span class="metric-label">Pressure</span>
<span class="metric-value">{Math.round(current.pressure_msl || 0)} hPa</span> <span class="metric-value">{Math.round(current.pressure_msl || 0)} hPa</span>
</div> </div>
<div class="metric-card"> <div class="metric-card" title={fbUv ? 'UV index from Open-Meteo — NWS does not provide it' : undefined}>
<span class="metric-icon">☀️</span> <span class="metric-icon">☀️</span>
<span class="metric-label">UV Index</span> <span class="metric-label">UV Index</span>
<span class="metric-value">{current.uv_index != null ? Math.round(current.uv_index) : '--'}</span> <span class="metric-value">{current.uv_index != null ? Math.round(current.uv_index) : '--'}</span>

View File

@ -9,6 +9,8 @@
const unit = $derived(app.settings.units === 'imperial' ? '°F' : '°C') const unit = $derived(app.settings.units === 'imperial' ? '°F' : '°C')
const windUnit = $derived(app.settings.units === 'imperial' ? 'mph' : 'km/h') const windUnit = $derived(app.settings.units === 'imperial' ? 'mph' : 'km/h')
const fbPressure = $derived(!!data?._fallback?.pressure)
const fbUv = $derived(!!data?._fallback?.uv)
function windCompass(deg) { function windCompass(deg) {
if (deg == null) return '--' if (deg == null) return '--'
@ -95,7 +97,7 @@
</div> </div>
<div class="detail-row"> <div class="detail-row">
<span>Pressure</span> <span>Pressure</span>
<span class="detail-value">{Math.round(current.pressure_msl || 0)} hPa</span> <span class="detail-value" title={fbPressure ? 'Pressure from Open-Meteo — NWS does not provide it' : undefined}>{Math.round(current.pressure_msl || 0)} hPa</span>
</div> </div>
<div class="detail-row"> <div class="detail-row">
<span>Feels Like</span> <span>Feels Like</span>
@ -117,7 +119,7 @@
<div class="detail-body"> <div class="detail-body">
<div class="detail-row"> <div class="detail-row">
<span>UV Index</span> <span>UV Index</span>
<span class="detail-value"> <span class="detail-value" title={fbUv ? 'UV index from Open-Meteo — NWS does not provide it' : undefined}>
{current.uv_index != null ? Math.round(current.uv_index) : '--'} {current.uv_index != null ? Math.round(current.uv_index) : '--'}
{#if current.uv_index != null} {#if current.uv_index != null}
<span class="uv-badge uv-{getUVLevel(current.uv_index).toLowerCase().replace(' ', '-')}">{getUVLevel(current.uv_index)}</span> <span class="uv-badge uv-{getUVLevel(current.uv_index).toLowerCase().replace(' ', '-')}">{getUVLevel(current.uv_index)}</span>
@ -134,7 +136,7 @@
</div> </div>
<div class="detail-row"> <div class="detail-row">
<span>Max UV Today</span> <span>Max UV Today</span>
<span class="detail-value">{daily?.uv_index_max?.[0] != null ? Math.round(daily.uv_index_max[0]) : '--'}</span> <span class="detail-value" title={fbUv ? 'UV from Open-Meteo — NWS does not provide it' : undefined}>{daily?.uv_index_max?.[0] != null ? Math.round(daily.uv_index_max[0]) : '--'}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -252,3 +252,29 @@ export async function fetchForecastNWS(lat, lon, units = 'imperial') {
return normalizeNWS({ points, forecast, hourly, grid, timeZone: tz, units }) return normalizeNWS({ points, forecast, hourly, grid, timeZone: tz, units })
} }
/**
* Backfill fields that NWS does not provide (pressure, UV index) from an
* Open-Meteo response, tagging them so the UI can show a tooltip that the
* value came from the non-selected source.
*/
export function mergeFallbackData(nwsData, omData) {
const fallback = {}
const cur = nwsData.current || {}
const omCur = omData?.current || {}
if (omCur.pressure_msl != null) {
cur.pressure_msl = omCur.pressure_msl
fallback.pressure = true
}
if (omCur.uv_index != null) {
cur.uv_index = omCur.uv_index
fallback.uv = true
}
const omDaily = omData?.daily || {}
if (omDaily.uv_index_max?.length && nwsData.daily) {
nwsData.daily.uv_index_max = omDaily.uv_index_max
fallback.uv = true
}
if (Object.keys(fallback).length) nwsData._fallback = fallback
return nwsData
}

View File

@ -4,7 +4,7 @@
import { getLocations, loadSettings } from '../storage/db.js' import { getLocations, loadSettings } from '../storage/db.js'
import { fetchForecast } from '../api/weather.js' import { fetchForecast } from '../api/weather.js'
import { fetchForecastNWS } from '../api/nws.js' import { fetchForecastNWS, mergeFallbackData } from '../api/nws.js'
export class AppStore { export class AppStore {
// Location state // Location state
@ -116,6 +116,14 @@ export class AppStore {
if (source === 'nws') { if (source === 'nws') {
try { try {
data = await fetchForecastNWS(location.lat, location.lon, units) data = await fetchForecastNWS(location.lat, location.lon, units)
// NWS omits pressure + UV index — backfill those from Open-Meteo and
// flag them so the UI can tooltip that they came from the other source.
try {
const om = await fetchForecast(location.lat, location.lon, units)
data = mergeFallbackData(data, om)
} catch (omErr) {
console.warn('Open-Meteo fields unavailable:', omErr.message)
}
} catch (nwsErr) { } catch (nwsErr) {
// NWS is US-only and can be unavailable — fall back to Open-Meteo // NWS is US-only and can be unavailable — fall back to Open-Meteo
// so the app keeps working rather than breaking. // so the app keeps working rather than breaking.

View File

@ -8,6 +8,7 @@ import {
dateKey, dateKey,
localTimeString, localTimeString,
normalizeNWS, normalizeNWS,
mergeFallbackData,
} from '../src/lib/api/nws.js' } from '../src/lib/api/nws.js'
const FIX = (name) => JSON.parse(readFileSync(path.join('tests/fixtures/nws', name), 'utf8')) const FIX = (name) => JSON.parse(readFileSync(path.join('tests/fixtures/nws', name), 'utf8'))
@ -103,3 +104,26 @@ describe('normalizeNWS (recorded live fixture for Rogers County, OK)', () => {
expect(m.current.wind_speed_10m).toBeCloseTo(10 * 1.609344, 1) expect(m.current.wind_speed_10m).toBeCloseTo(10 * 1.609344, 1)
}) })
}) })
describe('mergeFallbackData', () => {
it('backfills pressure + UV from Open-Meteo and tags them (non-selected source)', () => {
const nws = {
_source: 'nws',
current: { temperature_2m: 103, pressure_msl: null, uv_index: null },
daily: { time: ['2026-08-16'], uv_index_max: [] },
}
const om = { current: { pressure_msl: 1011.2, uv_index: 7.4 }, daily: { uv_index_max: [7.2, 6, 5] } }
const r = mergeFallbackData(nws, om)
expect(r.current.pressure_msl).toBe(1011.2)
expect(r.current.uv_index).toBe(7.4)
expect(r.daily.uv_index_max).toEqual([7.2, 6, 5])
expect(r._fallback).toMatchObject({ pressure: true, uv: true })
})
it('does not tag when Open-Meteo lacks the fields', () => {
const nws = { current: { pressure_msl: null, uv_index: null }, daily: {} }
const om = { current: { pressure_msl: null, uv_index: null }, daily: {} }
const r = mergeFallbackData(nws, om)
expect(r._fallback).toBeUndefined()
})
})