154 lines
4.9 KiB
JavaScript
154 lines
4.9 KiB
JavaScript
/**
|
|
* Weather alert / notification store using Svelte 5 runes.
|
|
* Monitors forecast data and generates alert banners.
|
|
* Dismissed alerts persist in IndexedDB and only reset when the forecast date changes.
|
|
*/
|
|
|
|
import { isSevereWeather } from '../api/weather-codes.js'
|
|
import { app } from './app.svelte.js'
|
|
import { saveSettings } from '../storage/db.js'
|
|
import { parseLocalDate } from '../dates.js'
|
|
|
|
export class NotificationStore {
|
|
/** @type {Array<{ id: string, type: string, severity: 'info'|'warning'|'danger', message: string, icon: string }>} */
|
|
alerts = $state([])
|
|
/** @type {Set<string>} */
|
|
dismissedIds = $state(new Set())
|
|
/** Track which forecast day last generated alerts — reset dismissals when the day changes */
|
|
lastForecastDate = null
|
|
/** Whether dismissals have been loaded from DB */
|
|
_dismissalsLoaded = false
|
|
|
|
/**
|
|
* Check forecast data and generate appropriate alerts.
|
|
*/
|
|
analyze() {
|
|
const data = app.forecastData
|
|
const settings = app.settings
|
|
|
|
if (!data || !settings.alertsEnabled) {
|
|
this.alerts = []
|
|
return
|
|
}
|
|
|
|
// Load persisted dismissals from DB on first analysis
|
|
if (!this._dismissalsLoaded) {
|
|
this._dismissalsLoaded = true
|
|
this._loadDismissed()
|
|
}
|
|
|
|
// Track the forecast date. If it changed, reset dismissals.
|
|
const forecastDate = data.daily?.time?.[0] || null
|
|
if (forecastDate && this.lastForecastDate && forecastDate !== this.lastForecastDate) {
|
|
this.resetDismissed()
|
|
}
|
|
this.lastForecastDate = forecastDate
|
|
|
|
const t = settings.alertThresholds
|
|
const newAlerts = []
|
|
|
|
function add(id, type, severity, message, icon) {
|
|
if (!app.settings.alertsEnabled) return
|
|
newAlerts.push({ id, type, severity, message, icon })
|
|
}
|
|
|
|
const current = data.current
|
|
const daily = data.daily || {}
|
|
|
|
// --- Current weather alerts ---
|
|
|
|
if (t.thunderstorm && isSevereWeather(current.weather_code)) {
|
|
add('storm-now', 'storm', 'danger',
|
|
'⚡ Thunderstorm active — seek shelter if outdoors', '⛈️')
|
|
}
|
|
|
|
// --- Daily forecast alerts ---
|
|
|
|
if (daily.time) {
|
|
for (let i = 0; i < daily.time.length && i < 3; i++) {
|
|
const day = daily.time[i]
|
|
const dateLabel = i === 0 ? 'Today' : i === 1 ? 'Tomorrow' : parseLocalDate(day).toLocaleDateString('en-US', { weekday: 'short' })
|
|
|
|
const precip = daily.precipitation_probability_max?.[i] || 0
|
|
if (precip >= t.precip) {
|
|
add(`rain-${i}`, 'precip', 'warning',
|
|
`🌧️ ${precip}% chance of rain ${dateLabel.toLowerCase()}`, '🌧️')
|
|
}
|
|
|
|
const code = daily.weather_code?.[i]
|
|
if (t.thunderstorm && isSevereWeather(code)) {
|
|
add(`storm-${i}`, 'storm', 'danger',
|
|
`⛈️ Thunderstorms expected ${dateLabel.toLowerCase()}`, '⛈️')
|
|
}
|
|
|
|
const tempHigh = daily.temperature_2m_max?.[i]
|
|
if (tempHigh != null && tempHigh > t.tempHigh) {
|
|
add(`heat-${i}`, 'heat', 'warning',
|
|
`🔥 ${dateLabel} high of ${Math.round(tempHigh)}° — stay hydrated`, '🔥')
|
|
}
|
|
|
|
const tempMin = daily.temperature_2m_min?.[i]
|
|
if (tempMin != null && tempMin < t.tempLow) {
|
|
add(`cold-${i}`, 'cold', 'warning',
|
|
`🥶 ${dateLabel} low of ${Math.round(tempMin)}° — bundle up`, '❄️')
|
|
}
|
|
|
|
const uv = daily.uv_index_max?.[i]
|
|
if (uv != null && uv >= t.uvIndex) {
|
|
add(`uv-${i}`, 'uv', 'warning',
|
|
`☀️ High UV index (${uv}) ${dateLabel.toLowerCase()} — wear sunscreen`, '🧴')
|
|
}
|
|
|
|
const wind = daily.wind_speed_10m_max?.[i]
|
|
if (wind != null && wind >= t.windGust) {
|
|
add(`wind-${i}`, 'wind', 'warning',
|
|
`💨 Strong winds (${Math.round(wind)} ${app.settings.units === 'imperial' ? 'mph' : 'km/h'}) ${dateLabel.toLowerCase()}`, '💨')
|
|
}
|
|
}
|
|
}
|
|
|
|
this.alerts = newAlerts.filter((a) => !this.dismissedIds.has(a.id))
|
|
}
|
|
|
|
/**
|
|
* Dismiss a specific alert. Persists to IndexedDB.
|
|
* @param {string} alertId
|
|
*/
|
|
dismiss(alertId) {
|
|
this.dismissedIds.add(alertId)
|
|
this.alerts = this.alerts.filter((a) => a.id !== alertId)
|
|
this._persistDismissed()
|
|
}
|
|
|
|
/**
|
|
* Reset all dismissed alerts (e.g., when forecast date changes).
|
|
*/
|
|
resetDismissed() {
|
|
this.dismissedIds.clear()
|
|
this._persistDismissed()
|
|
}
|
|
|
|
/**
|
|
* Load dismissed alert IDs from the persisted settings.
|
|
*/
|
|
_loadDismissed() {
|
|
const saved = app.settings.dismissedAlerts || []
|
|
this.dismissedIds = new Set(saved)
|
|
}
|
|
|
|
/**
|
|
* Save dismissed alert IDs to settings in IndexedDB.
|
|
*/
|
|
async _persistDismissed() {
|
|
const arr = Array.from(this.dismissedIds)
|
|
app.settings.dismissedAlerts = arr
|
|
try {
|
|
await saveSettings(app.settings)
|
|
} catch (e) {
|
|
console.error('Failed to persist dismissed alerts:', e)
|
|
}
|
|
}
|
|
}
|
|
|
|
export const notifications = new NotificationStore()
|