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.
- nws.js: new fetchActiveAdvisories() hits the official NWS alerts feed
(api.weather.gov /alerts/active?point=lat,lon) and normalizes each advisory
into the app's alert shape, tagged source:'nws'. Adds classifyAdvisoryEvent
(maps NWS events -> app buckets: heat/wind/cold/storm/precip, else advisory),
nwsSeverity (Extreme/Severe->danger etc.), advisoryId (stable short id from
the URN), advisoryIcon, and local effectiveDate/expiresDate for dedup.
- notifications store: cached _nwsAdvisories + refreshAdvisories() (best
effort, cached 5 min, non-US/no-alerts safe). analyze() now merges
advisories via new pure mergeAdvisories(): an advisory that is officially
in force on a date supersedes the derived forecast alert of the same type
on that date, so the official NWS warning replaces the forecast guess. Both
are dismissible independently (nws-* vs forecast-* ids).
- New per-type toggle "advisory" (defaults on) across app/default settings and
SettingsDialog, so official advisories are configurable like every other
alert and the bell shows them with their own severity/icon.
- Wired: App.svelte calls refreshAdvisories() alongside analyze() on forecast
load and changes.
- Tests: 12 advisory tests (classifier, severity, id, normalize, dedup incl.
disable toggle) + 3 store integration tests (overlap dedup, non-overlap kept,
toggle-off). 107 total pass.
- Verified end-to-end in headless Chromium against the live NWS feed for
Rogers County, OK: the current Heat Advisory appears in the bell, and the
derived heat alerts for the days it covers are suppressed while a
non-overlapping day's forecast heat alert remains.
Drop the 'Hourly'/'Daily'/etc. peek chip shown at the edge while swiping.
The live finger-tracking slide remains; only the label affordance is
removed.
Replace the instant-jump swipe with a live drag on touch: the current tab
content translates with the finger in real time (translateX driven by
touchmove), a peek chip at the edge shows the destination tab, and on
release the sheet animates to the next tab or snaps back. Horizontal vs
vertical travel is decided after 12px so normal page scrolling is
untouched; touch-action:pan-y keeps horizontal gestures JS-owned. Radar
still pans its map instead of switching tabs.
Touch swipe on the content area moves between tabs (left = next, right =
previous), gated to non-radar views so map pan gestures keep working.
Vertical scroll with |dx|<|dy| is untouched; 60px horizontal threshold.
- New src/lib/router.js: parseHash/ viewToHash for the 5 tab views, with a
default fallback for empty/unknown hashes.
- App.svelte: initialize viewMode from location.hash, setView() updates both
state and the URL hash, and a guarded hashchange listener syncs back/forward
and manual URL edits. All tab onclicks route through setView().
- Tests for parse/viewToHash round-trips and fallback. 76 total pass.