/** * 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', refreshInterval: 30, alertsEnabled: true, showLightning: false, // toggleable lightning strike layer on the radar map notificationTypes: { storm: true, precip: true, heat: true, cold: true, uv: true, wind: true, }, alertThresholds: { precip: 70, windGust: 40, uvIndex: 6, tempHigh: 35, tempLow: 0, }, }) /** * 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 } } /** * Start (or restart) the auto-refresh timer based on the configured * interval. A value of 0 (or missing) disables auto-refresh. */ startAutoRefresh() { this.stopAutoRefresh() const minutes = this.settings.refreshInterval if (!minutes || minutes <= 0) return this._refreshTimer = setInterval(() => { this.refreshForecast() }, minutes * 60 * 1000) } /** * Stop the auto-refresh timer if it is running. */ stopAutoRefresh() { if (this._refreshTimer) { clearInterval(this._refreshTimer) this._refreshTimer = null } } /** * 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()