weather_app/AGENTS.md
hermes-explorigin 4616c297a9
All checks were successful
Test, Build & Deploy / test-and-build (push) Successful in 24s
Test, Build & Deploy / deploy (push) Successful in 18s
feat(storms): NWS alerts, SPC outlook and IEM storm reports
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.
2026-10-17 18:10:46 +00:00

137 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.