weather_app/src/lib/stores/app.svelte.js
hermes-explorigin c0ec45f170 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 / --.
2026-08-16 21:05:07 +00:00

186 lines
4.8 KiB
JavaScript

/**
* App-level reactive state using Svelte 5 runes.
*/
import { getLocations, loadSettings } from '../storage/db.js'
import { fetchForecast } from '../api/weather.js'
import { fetchForecastNWS, mergeFallbackData } from '../api/nws.js'
export class AppStore {
// Location state
locations = $state([])
selectedLocationId = $state(null)
selectedLocation = $derived(
this.locations.find((l) => l.id === this.selectedLocationId) || null
)
// Forecast data
forecastData = $state(null)
loading = $state(false)
error = $state('')
// Geocoding
geocodingResults = $state([])
geocodingLoading = $state(false)
geocodingError = $state('')
// UI state
showAddLocation = $state(false)
showSettings = $state(false)
sidebarOpen = $state(false)
// Settings
settings = $state({
units: 'metric',
source: 'open-meteo',
alertsEnabled: true,
alertThresholds: {
precip: 70,
windGust: 40,
uvIndex: 6,
tempHigh: 35,
tempLow: 0,
thunderstorm: true,
},
})
/**
* Initialize the app: load locations + settings.
*/
async init() {
// Load settings first so we can use unit for forecasts
try {
this.settings = await loadSettings()
} catch (e) {
console.error('Failed to load settings:', e)
}
// Load locations
try {
this.locations = await getLocations()
} catch (e) {
console.error('Failed to load locations:', e)
}
// Load forecast for selected or first location
if (this.locations.length > 0) {
const current = this.locations.find((l) => l.isCurrent)
if (current) {
this.selectedLocationId = current.id
} else {
this.selectedLocationId = this.locations[0].id
}
await this.refreshForecast()
}
}
/**
* Refresh settings from DB.
*/
async refreshSettings() {
try {
this.settings = await loadSettings()
} catch (e) {
console.error('Failed to refresh settings:', e)
}
}
/**
* Refresh the forecasts list from DB.
*/
async refreshLocations() {
try {
this.locations = await getLocations()
} catch (e) {
console.error('Failed to refresh locations:', e)
}
}
/**
* Fetch forecast data for the currently selected location.
*/
async refreshForecast(locationId = null) {
const locId = locationId || this.selectedLocationId
if (!locId) return
const location = this.locations.find((l) => l.id === locId)
if (!location) return
this.loading = true
this.error = ''
try {
const units = this.settings.units
const source = this.settings.source || 'open-meteo'
let data
if (source === 'nws') {
try {
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) {
// NWS is US-only and can be unavailable — fall back to Open-Meteo
// so the app keeps working rather than breaking.
console.warn('NWS unavailable, falling back to Open-Meteo:', nwsErr.message)
data = await fetchForecast(location.lat, location.lon, units)
}
} else {
data = await fetchForecast(location.lat, location.lon, units)
}
this.forecastData = data
// Update the location name if we got a timezone abbreviation
if (data.timezone_abbreviation && !location.timezone) {
// We could update the location with timezone info, but skip for now
}
} catch (e) {
this.error = e.message || 'Failed to fetch weather data'
console.error('Forecast fetch error:', e)
} finally {
this.loading = false
}
}
/**
* Select a location and fetch its forecast.
* @param {string} id
*/
async selectLocation(id) {
this.selectedLocationId = id
this.sidebarOpen = false
await this.refreshForecast(id)
}
/**
* Search locations via geocoding API.
* @param {string} query
*/
async searchGeocoding(query) {
if (!query || query.length < 2) {
this.geocodingResults = []
return
}
this.geocodingLoading = true
this.geocodingError = ''
try {
const { searchLocations } = await import('../api/weather.js')
this.geocodingResults = await searchLocations(query)
} catch (e) {
this.geocodingError = e.message || 'Search failed'
this.geocodingResults = []
} finally {
this.geocodingLoading = false
}
}
}
export const app = new AppStore()