Adds a Storms tab driven by three keyless, CORS-enabled feeds: - api.weather.gov/alerts/active?point=… for watches/warnings/advisories, with a banner above the content for tornado/severe/Extreme alerts - SPC Day 1-3 categorical outlooks plus Day 1 tornado/hail/wind probabilities, resolved by point-in-polygon (nolyr GeoJSON) - NWS Local Storm Reports via IEM (bounding box + radius cut), nearest first New: src/lib/api/storm.js (pure helpers + cached fetchers), src/lib/stores/storm.svelte.js (5-min polling, stale-response guard), src/components/StormWatch.svelte. Router/nav gain a 'storms' view. Tests: geometry/parsing (16), store (7), component render (3). Docs: AGENTS.md storm section + API notes.
9.4 KiB
9.4 KiB
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
$statewraps values in JavaScript Proxy objects. Any API that uses the structured clone algorithm (IndexedDBput()/add(),structuredClone(),postMessage(),history.pushState()) will throw"Proxy object could not be cloned"if given a$statevalue directly. To avoid this:- Never pass
$stateobjects directly to IndexedDB. Always spread into a plain object first:db.put('store', { ...proxyObj }). - Never use
structuredClone()on$statevalues. Use{ ...obj, nested: { ...obj.nested } }orJSON.parse(JSON.stringify(obj))for deep copies. - DB functions should sanitize inputs at the storage layer:
JSON.parse(JSON.stringify(settings))beforedb.put(). - Reading from IndexedDB returns plain objects (no proxies), so reads are safe.
- Never pass
- Open-Meteo API — Free forecast API (no key required). Two endpoints used:
api.open-meteo.com/v1/forecast— Current + hourly (24h) + daily (7 days) weathergeocoding-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.jsanalyzes 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:locationsandsettings. Current location pinned at top of sidebar. - Single-file build —
vite-plugin-singlefileinlines 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: falsein 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
(
LABELof 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 (orhours=) returns national reports.normalizeStormReportsthen 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, isSevereWeathertests/lib/storage/db.test.js— Locations CRUD (add, update, delete, setCurrent, clear), settings CRUD with defaultstests/lib/stores/app.test.js— AppStore initialization and derived selectedLocationtests/lib/stores/notifications.test.js— Alert generation for all alert types, dismissal, reset, day filteringtests/lib/api/storm.test.js— Point-in-polygon, risk ranking, SPC probabilities, LSR radius filter, alert sortingtests/lib/stores/storm.test.js— StormStore loading, caching, location switches, partial failures, non-US pointstests/components/storm-watch.test.js— StormWatch renders populated and empty states (Sveltemountin jsdom)
- Run with
npm run testornpm 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.geojsonnames. - NWS/SPC/IEM all send
Access-Control-Allow-Origin: *, so the static single-file build can call them without a proxy.