The grid id and URL omitted the model, so two models sharing a cycle produced
the same `/api/wind/grid/<id>.bin`. Because the grid route defaults to `gfs`,
an IFS grid request resolved against the GFS cache: when both cycles matched it
silently served the GFS field as IFS, and when they differed it 404'd and the
client parsed the error body as float data.
Include the model in both the id and the URL, and reject a resampled grid that
contains non-finite values instead of caching it. Verified against live data:
the IFS grid now decodes to 65,160 finite values (u -20.2..27.6 m/s) and renders
a field distinct from GFS.
Add ECMWF IFS 10 m wind alongside NOAA GFS, selectable at runtime.
The /api/wind proxy becomes a model dispatcher: `?model=gfs|ifs` (default gfs,
unknown → 400), cached per model+cycle+grid. IFS reads the keyless ECMWF Open
Data JSON Lines `.index` (note: `<stem>.index`, not `<grib2>.index`), byte-range
fetches only the `10u`/`10v` messages, decodes them with the same ecCodes WASM
path, and resamples with the existing grid code (ECMWF grids start at lo1=180;
the resampler already wraps columns).
The client layer gains a `model` parameter (setParams/getParams) with a
generation guard so a model switch aborts and refetches exactly once. Registered
as an enabled+options layer with a `model` enum.
Validated end-to-end against the real 2026-09-14 06z IFS cycle on Node 25
(180x91 grid at 2°, 131,040-byte payload, plausible mean |u|/|v|), and GFS
still passes the same check.
Attribution: contains modified Copernicus/ECMWF IFS forecast data, licensed
under CC BY 4.0. Verified: npm test (3412 pass, 0 fail), check:boundaries.
Add a Wind layer animating the global 10 m wind field from NOAA GFS.
The /api/wind proxy selects the latest available 0.25° cycle, reads its .idx
inventory, byte-range fetches only the UGRD/VGRD 10 m GRIB2 messages (~1 MB
each instead of the whole ~500 MB file), decodes them with ecCodes (WASM),
resamples to a compact grid (1° default), and serves a manifest plus a Float32
U/V payload cached per cycle for an hour (single-flight, last-good on failure).
The client renders nullschool-style particles in a canvas overlay: bilinear
field sampling with longitude wrap, per-frame advection, Cesium camera
projection, and globe-occlusion culling via EllipsoidalOccluder.
Validated end-to-end against the real 2026-09-14 06z cycle on Node 25
(180x91 grid at 2°, 131,040-byte payload, plausible mean |u|/|v|). The WASM
decoder requires Node >=24, which the repo already requires.
Registered as `wind` (token k) with attribution and docs. Verified: npm test
(3412 pass, 0 fail), npm run check:boundaries.