# WeatherLens — Agent Context ## Project Overview A single-file, offline-capable weather app built with **Svelte 5** (runes-based reactivity). Pulls data from the free **Open-Meteo API** (no API key required), persists user locations and settings in IndexedDB, and provides configurable weather alerts. The production build is a single `dist/index.html` with zero network dependencies — it works from `file://`, USB, or any static server. Weather data is fetched live from the CDN. ## Architecture ``` src/ ├── App.svelte # Root component — geolocation init, sidebar + main layout ├── main.js # Entry point, mounts App + imports CSS ├── components/ │ ├── LocationSidebar.svelte # Saved locations list, inline geocoding search │ ├── AddLocationDialog.svelte# Modal: geocoding search → add location │ ├── CurrentWeather.svelte # Hero card: current conditions, dynamic gradient bg │ ├── HourlyForecast.svelte # Horizontal scroll: next 24h with precip bars │ ├── DailyForecast.svelte # 7-day forecast cards with icons + detail │ ├── WeatherDetail.svelte # Expanded metrics: wind, atmosphere, sun/UV, temps │ ├── NotificationBell.svelte # Bell icon with badge; dropdown lists/dismisses alerts │ ├── StormWatch.svelte # Storms tab: active alerts, SPC outlook, recent reports │ └── SettingsDialog.svelte # Units (metric/imperial), alert thresholds, data reset ├── lib/ │ ├── autofocus.js # Svelte action for autofocus on mount │ ├── api/ │ │ ├── weather.js # Open-Meteo fetch functions (forecast + geocoding) │ │ ├── nws.js # NWS forecast + official advisories normalization │ │ ├── storm.js # NWS alerts, SPC outlooks, IEM storm reports (+ geo helpers) │ │ ├── moon.js # Moon phase calculation │ │ └── weather-codes.js # WMO code → icon/description/severity mapping │ ├── storage/ │ │ └── db.js # IndexedDB layer (idb): locations, settings │ └── stores/ │ ├── app.svelte.js # AppStore: $state for locations, forecast, UI, settings │ ├── storm.svelte.js # StormStore: alerts/outlook/reports for the selected point │ └── notifications.svelte.js # Alert generation from forecast data ``` ## Key Design Decisions - **Svelte 5 runes** — Use `$state`, `$derived`, `$effect`. Props-based event passing. No legacy Svelte events. - **$state proxy pitfall** — Svelte 5 `$state` wraps values in JavaScript Proxy objects. Any API that uses the structured clone algorithm (IndexedDB `put()`/`add()`, `structuredClone()`, `postMessage()`, `history.pushState()`) will throw `"Proxy object could not be cloned"` if given a `$state` value directly. To avoid this: - **Never pass `$state` objects directly to IndexedDB.** Always spread into a plain object first: `db.put('store', { ...proxyObj })`. - **Never use `structuredClone()` on `$state` values.** Use `{ ...obj, nested: { ...obj.nested } }` or `JSON.parse(JSON.stringify(obj))` for deep copies. - **DB functions should sanitize inputs** at the storage layer: `JSON.parse(JSON.stringify(settings))` before `db.put()`. - **Reading from IndexedDB returns plain objects** (no proxies), so reads are safe. - **Open-Meteo API** — Free forecast API (no key required). Two endpoints used: - `api.open-meteo.com/v1/forecast` — Current + hourly (24h) + daily (7 days) weather - `geocoding-api.open-meteo.com/v1/search` — City/location search by name - **Weather state via emoji** — All weather icons use Unicode emoji, so no icon fonts or image assets are needed. WMO codes map to emoji via `weather-codes.js`. - **Dynamic backgrounds** — CurrentWeather uses CSS gradient classes (`weather-bg-clear`, `weather-bg-rain`, `weather-bg-storm`, etc.) determined by weather severity. - **Alert/notification system** — `notifications.svelte.js` analyzes forecast data each time it changes and generates alerts for: high precipitation, thunderstorms, extreme temps, high UV, strong winds. Alerts are shown only in the 🔔 bell dropdown. Dismissed alerts persist in IndexedDB and only auto-reset when the forecast date changes (so refreshing the same day won't resurrect cleared alerts). - **Geolocation on first launch** — If no locations are saved, the app requests browser geolocation and adds "Current Location" automatically. Falls back to manual "Add Location" if denied. - **Locations persist in IndexedDB** — Via `idb`. Two stores: `locations` and `settings`. Current location pinned at top of sidebar. - **Single-file build** — `vite-plugin-singlefile` inlines all JS/CSS; post-build script inlines favicon as base64 data URI. - **Mobile-first responsive** — Slide-over sidebar on mobile (same pattern as password manager). Bottom tab nav on mobile, desktop tab bar on wide screens. Touch-friendly with large tap targets. - **No minification** — `minify: false` in vite.config.js for readable single-file output. ## Weather Alert System The notification store checks each forecast refresh for: | Alert Type | Trigger | Severity | |---|---|---| | Thunderstorm (current) | WMO code 65, 75, 82, 86, 95-99 active now | danger | | Thunderstorm (forecast) | WMO code 65, 75, 82, 86, 95-99 in next 3 days | danger | | Precipitation | precipitation_probability_max ≥ threshold (default 70%) | warning | | High Temperature | temperature_2m_max > threshold (default 35°C) | warning | | Low Temperature | temperature_2m_min < threshold (default 0°C) | warning | | UV Index | uv_index_max ≥ threshold (default 6) | warning | | Wind | wind_speed_10m_max ≥ threshold (default 40 km/h) | warning | All thresholds are configurable in Settings. ## Storm Watch (SPC / NWS / IEM) The **Storms** tab (and the red banner above the content when a tornado, severe thunderstorm or other Extreme alert is in force) is driven by three keyless, CORS-enabled feeds. All of them are fetched directly from the browser. | Source | Endpoint | Refresh | |---|---|---| | NWS active alerts | `api.weather.gov/alerts/active?point={lat},{lon}` | 2 min HTTP cache | | SPC Day 1–3 outlook | `spc.noaa.gov/products/outlook/day{1,2,3}otlk_{cat,torn,wind,hail}.nolyr.geojson` | 20 min HTTP cache | | NWS Local Storm Reports | `mesonet.agron.iastate.edu/geojson/lsr.geojson?west=…&sts=…&ets=…` | 5 min HTTP cache | - **SPC risk at a point** comes from point-in-polygon on the GeoJSON features (`LABEL` of the *highest* containing category wins: TSTM < MRGL < SLGT < ENH < MDT < HIGH). Day-1 tornado/hail/wind probabilities use the same test, with the "Less Than 2% All Areas" background polygon ignored. - **There is no Skywarn API** — Skywarn is a per-WFO volunteer program. The equivalent machine-readable ground truth is NWS Local Storm Reports (LSRs), which include trained-spotter and Spotter Network submissions. - The IEM LSR query **must include `sts`/`ets`**; a bbox alone (or `hours=`) returns national reports. `normalizeStormReports` then applies the exact radius cut (160 km by default) and sorts nearest-first. - Non-US points skip the SPC/LSR requests (`isUsPoint`) and the UI explains the US-only coverage. - The store de-dupes concurrent loads, ignores stale responses after a location switch, and polls every 5 min only while the document is visible. ## Scripts | Command | Description | |---|---| | `npm run dev` | Vite dev server with HMR on `:5173` | | `npm run build` | Production build → `dist/index.html` (single self-contained file) | | `npm run preview` | Preview production build | | `npm run test` | Vitest (watch mode) | | `npm run test:run` | Vitest (single run) | ## Testing - Framework: **Vitest** with **jsdom** environment - Test setup: `tests/setup.js` (fake-indexeddb polyfill, Web Crypto API polyfill, mock matchMedia, mock navigator) - Test files: - `tests/lib/api/weather-codes.test.js` — WMO code → icon/desc/severity mapping, isSevereWeather - `tests/lib/storage/db.test.js` — Locations CRUD (add, update, delete, setCurrent, clear), settings CRUD with defaults - `tests/lib/stores/app.test.js` — AppStore initialization and derived selectedLocation - `tests/lib/stores/notifications.test.js` — Alert generation for all alert types, dismissal, reset, day filtering - `tests/lib/api/storm.test.js` — Point-in-polygon, risk ranking, SPC probabilities, LSR radius filter, alert sorting - `tests/lib/stores/storm.test.js` — StormStore loading, caching, location switches, partial failures, non-US points - `tests/components/storm-watch.test.js` — StormWatch renders populated and empty states (Svelte `mount` in jsdom) - Run with `npm run test` or `npm run test:run` ## API Notes - Open-Meteo is free and requires no API key. - Forecast parameters: current (10 metrics), hourly (4 metrics × time), daily (8 metrics × 7 days) - Geocoding API returns up to 8 results per query with name, coordinates, country, admin1, timezone. - Rate limits are generous (~10,000 requests/day for the free tier). - Used only via `fetch()` in browser — no server-side API calls needed. - SPC outlook files are regenerated at each issuance (roughly 12Z/13Z/16:30Z/01Z); never hardcode a timestamp, just re-fetch the stable `.nolyr.geojson` names. - NWS/SPC/IEM all send `Access-Control-Allow-Origin: *`, so the static single-file build can call them without a proxy.