Conversation
Deploying dynamical-org with
|
| Latest commit: |
f4f7a62
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://14f05039.dynamical-org.pages.dev |
| Branch Preview URL: | https://feat-deckgl-explorer.dynamical-org.pages.dev |
…directly An "Explore" section on enabled dataset pages loads a deck.gl map behind a "Load interactive map" button. The explorer (explorer/, built by Vite into public/explorer/ during npm run build) opens the dataset's icechunk-https asset with icechunk-js + zarrita, synthesizes GeoZarr attrs from the store's own coordinates (lat/lon, Lambert conformal, rotated pole), and renders with @developmentseed/deck.gl-zarr: a variable select, a lead or time slider, and a select per extra dim. Only noaa-gfs-forecast is enabled in this commit. Also staged, not yet wired in: a gribberish wasm codec (explorer/gribwasm, explorer/src/grib) for the virtual stores. Tests: offline unit tests for the pure modules and the GRIB codec; offline Playwright specs against a tiny generated Icechunk fixture; a live GFS smoke spec.
…eventy The Cloudflare Pages build doesn't run `npm run build`, so the explorer bundle never reached the preview. eleventy.before now builds explorer/ when its bundle is missing or older than its sources, whichever command runs eleventy. _data/explorer.js now lists every materialized catalog product with its initial view, default variable and the measured compressed bytes for that view. HRRR analysis and MRMS open on a Houston-area view: their CONUS views are about 1.5 GB and 387 MB.
Review pass 1: the build hook took an existing explorer/node_modules as proof it matched the lockfile, so a dependency bump or a half-finished install built with stale packages.
Virtual stores: the gribberish wasm codec registers lazily (materialized pages never fetch it). Whole-grid chunks are sliced into small tiles through a facade, because one globe-sized tile misregisters by 1–3 cells when deck.gl-raster's mesh refinement hits its iteration cap. Upstream NOAA/ECMWF reads retry 429/5xx with backoff. A virtual store's latest run is the newest init whose final lead exists. Not enabled on any page yet. Review pass 1: 1. rejected coordinate/grid/metadata promises are evicted, so Retry recovers; 2. changing the selection while unloaded no longer strands the map; 3. an analysis whose newest window is empty searches back for data instead of opening a blank frame; 4. the GPU-memory guard warns with "Load anyway" instead of blocking.
Second-look on review pass 1: an mtime freshness check can skip a build whose output is partial or whose sources were deleted. Vite takes about a second, so build unconditionally once per process. npm ci now keys on package.json and package-lock.json and on Vite being installed. Vite 8 needs Node ^20.19 or >=22.12, so .node-version pins Pages to 22, the version the explorer was tested on.
…l data Second-look on review pass 1: "Load anyway" still gated loading, against the no-limits staging scope. Above the estimate the explorer now keeps loading and shows a warning line. A constant sample (MRMS on a dry day) no longer freezes a 0..1 range: the first varying block sets it. Facade cache keys carry the snapshot, variable path and every non-spatial index, and one tile's abort never cancels a shared read. Retry wording no longer guesses at throttling.
The virtual stores get Explore sections, with captions that say the download is store metadata plus the GRIB messages it points to. HRRR analysis and MRMS captions name their Houston-area initial view. The Explore box no longer widens phones past the viewport (aspect-ratio with a min-height needed width: 100%). New offline specs: Retry after a failed coordinate read; a selection change while unloaded; an analysis whose data stops a window before its end; a failed pan within a block and a failed block change after the first frame. The live spec also loads noaa-gfs-forecast-virtual.
Review pass 2: the hook's install-and-build contract had no regression test. lib/explorer-build.js takes an injectable exec. The new tests cover: - one install and one build per process; - rebuild-only when the install matches; - reinstall on a package.json or lockfile change, or a missing Vite; - no marker after a failed install; - a failed build throwing and retrying.
…t lead 1. While unloaded, variable, level and step changes compose onto one pending selection. The pending variable's own controls are rebuilt from metadata, and the old variable's controls can no longer revert it. 2. The whole-grid facade separates in-flight reads from the resolved LRU and counts waiters. A read is aborted when its last tile leaves. clear() (on variable/level change, Unload and destroy) aborts running reads and drops queued ones. 3. A forecast whose opening step is all-missing (a virtual accumulation at +0 h) searches the following steps and opens on the first with data. 4. A late rejection no longer deletes a newer cache entry for the same key. Also: the retry backoff aborts with the request, and an upstream outage during the latest-run probe is reported as a failed probe, not "no run".
Specs for a variable and level chosen in one unloaded interval, the old variable's controls not reverting the pending one, a one-step-chunk forecast opening past an empty first lead, the advisory GPU estimate, and out-of-order draws through the whole-grid facade. The fixture gains a one-step-chunk forecast array.
1. Switching variable while one is loaded removes the old variable's level selects and disables the slider at once. Control events count only for the requested selection. A failed switch keeps the previous field and restores its dropdown; Retry retries the failed choice. 2. Each facade owner generation has its own limiter, so reads abandoned by a cleared generation, even ones that never settle, can't starve the replacement's slots.
Changing the old level while a new variable's reads are held must still draw the new variable, with its dropdown and legend in agreement. This spec fails on 82571bb.
CI's render job (Node 24, npm 11) refused 'npm ci': the lockfile listed only the linux-x64-gnu builds of rolldown and lightningcss (and no fsevents), so npm 11 saw it as out of sync with package.json. Regenerated with npm 11 --package-lock-only: 25 entries added, and no locked version changed. npm ci now passes with npm 11 and npm 10.
…top loading Marsh saw GEFS 35-day fail at its 6-hourly steps. The cause is a partly written newest run: its later lead chunks exist but hold only NaN, and the explorer reported Ready over a blank map. Each uploaded block now records which steps have values. When nothing in view has a value at the chosen step, the state is "empty", and the status says where values end and that an earlier init may have them. - Init time: a select of the variable's newest 20 init_time values at the pinned snapshot (the default is always included). An explicit choice never falls back to another run or advances the lead. Missing data and transport failure are distinct states, and Retry repeats the exact selection. - ensemble_member is a select labelled with coordinate values; the default is member 0 if the coordinate has it. - The Unload/Load mode is gone. A "Stop loading" button, shown only while loading, aborts reads, keeps the drawn field and offers Retry. - The strip reserves its label rows before the fit, so the mounted map's zoom matches the server-rendered preview.
Marsh asked for the map, empty, with the button over it, instead of the example image. Each Explore box now inlines a build-time SVG of world-atlas borders: - projected to Web Mercator and fitted exactly as the explorer fits its initial view; - clipped at build time, adding 0.5–6 KB gzip per page, less than the thumbnail JPEG it replaces; - drawn with site tokens and aria-hidden. The explorer bundle and weather reads still wait for the click. Specs that exercised Unload now cover the same logic through selection replacement and destroy.
Two slider moves in one frame, away from the drawn block and back, left that block's layer both live and queued for retirement. deck kept the instance, since it never saw a frame without it, but onAfterRender destroyed its textures and forgot it had loaded. The field then drew from destroyed textures and the state stayed 'loading' for good. This pre-dates 1f31b27; the facade stale-draw spec hit it intermittently. Skip ids that are live again, and add a spec that makes both moves in one frame.
CodeQL flagged url.includes("amazonaws.com") as incomplete URL substring
sanitization. It is only a test assertion, but a hostname suffix check is
the correct form.
…tartup Focused review of Marsh's feedback delta: 1. Stop now holds a stopped state until Retry or a new selection. Queued tile callbacks and pans refuse before reading, and late replies refuse before uploading. The facade drops its queue; drawn textures stay. 2. A slider move during a same-variable init/member/level change composes into the requested selection and supersedes the pending apply, so the drawn step and Retry follow the last input. 3. start() re-checks its generation after every await, so Stop during startup isn't undone when metadata arrives. Retry resumes startup. 4. "No data" is judged from the viewport's selected tiles (onViewportLoad), so cached off-screen tiles no longer make a blank view read Ready.
The old 'level chosen while a new variable loads' spec never changed a level during a pending read; the new spec holds the 850 hPa reads and re-picks 500 hPa during the hold. The fixture gains temperature_ensemble (members 0, 10, 20 with distinct values); a spec switches members and checks the drawn value and the coordinate-value label.
Marsh asked to try deck.gl on mobile. The Explore button now shows at every width and pointer type, and the small-screen note is gone. The phone spec now loads the map at 390 px and checks it draws without widening the page.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Deck.gl data explorer in the catalog — plan
PR: #250 (draft) · Issue: dynamical-org/meta#236 · Branch:
feat/deckgl-explorerGoal
Visitors to a catalog dataset page can see the data itself: a map of one variable, read straight from our published Icechunk store in the browser with deck.gl. No tile server, proxy or pre-rendered images.
Where this stands
noaa-gfs-forecasttemperature_2mstraight from the live Icechunk store in a browser, with values matching xarray exactly. Evidence and screenshots are below.Decisions (Marsh, 2026-09-25)
Tracks
Track 1: materialized products (every one the explorer can read)
The explorer core (
explorer/, plain JS ESM built by Vite intopublic/explorer/) is generic over dims. It covers:lead_timeortimesliders;ensemble_memberselect (default member 0);pressure_level;The catalog integration adds the Explore section for enabled products, lazy-loads on click, quotes a per-product "about N MB to start", and shows a note instead of the button on mobile.
Tests: offline unit tests; offline e2e against a tiny fixture Icechunk store; a live smoke test on GFS.
All 16 materialized products render (headless Chromium, 758-px map). The "Estimated … ~N MB" caption on each page comes from this table:
ob_tran)A GPU-memory estimate is shown as a warning when a view needs a lot of texture memory. It never blocks loading (Marsh: no limits in staging).
Track 2: virtual products (10
-virtualstores)gribberishcodec (honouringvar,adjust_longitude_range,north_up).scale_offsetis already built into zarrita 0.7.5.gribberishcodec: DRS 5.0/5.2/5.3 plus bitmap, grid 3.30 only, no WASM or COOP/COEP.src/lib/grib/?repoobject withno-cache, so a reload never opens a stale snapshot; probe the final lead so a virtual store opens on a run whose final lead exists (this doesn't prove every lead, member or variable is written).What the research established (2026-09-25)
Stack.
ZarrLayertakes a zarrita array, a selection for the non-spatial dims and GeoZarr-style spatial metadata. It tiles by inner chunk and reprojects on the GPU (WebGL2).@developmentseed/deck.gl-zarr/deck.gl-raster/geozarr0.8.1,@deck.gl/*9.4.0,@luma.gl/*9.4.2,zarrita0.7.5,icechunk-js0.6.0.Prior art.
dynamical-zarr-ecmwfexample renders our legacy (pre-Icechunk) ECMWF zarr. Its synthetic transform has nospatial:registration. For our GFS grid that setting was required, so the example may be half a cell off; this needs checking against the legacy store before any upstream report.nldas-icechunkshows the Icechunk store path.Our stores.
icechunk-httpsasset on regional S3.dynamical-*buckets: HEAD, OPTIONS withrange, and Range GET returning 206. Browser reads were exercised on GFS only.temperature_2mis(init, lead, lat, lon)with chunks(1,105,121,121)and shards(1,210,726,726), blosc-zstd. So one tile is 30°×30° × 105 leads: 6.1 MB decoded, 0.7–1.5 MB compressed.temperature_2marray usesscale_offset+gribberishcodecs overs3://noaa-gfs-bdp-pdsGRIB byte ranges. Other virtual arrays were not inspected.ingested_forecast_lengthis all-NaN, so it can't be used as a completeness signal.Site.
public/*.mjsor inline.public/vendor/already self-hosts versioned ESM with immutable caching.pipeline.spec.mjs).Spike evidence (GFS
temperature_2m, headless Chromium + SwiftShader)Screenshots
The images live on the orphan branch
screenshots/deckgl-explorer; delete it after merge.Spike (throwaway page, not site styling):
Other spike shots:
Placement mockups (decision 4; the spike map is embedded as-is, so its control panel is oversized):
More mockup shots:
Mockup notes:
Implementation plan
explorer/: plain JS ESM with JSDoc, sonpm testimports its pure modules directly.explorer/package-lock.json. That is a deliberate exception to the repo's gitignored-lockfile policy, so the tested dependency set (including the single proj4 instance) survives Pages rebuilds. It records every platform's optional native bindings (rolldown, lightningcss, fsevents), which CI's npm 11 requires fornpm ci.eleventy.beforehook in.eleventy.jsbuilds it with Vite intopublic/explorer/(gitignored), once per Eleventy process.npm ciwheneverpackage.jsonorpackage-lock.jsondiffers from the last successful install, or Vite is missing.npm run build; the first preview shipped without the bundle..node-versionpins Pages to Node 22, which Vite 8 needs and which the explorer was tested on.npm start, restart after editingexplorer/.explorer/src/grib/, with SHA256SUMS). Its crate source and pinned build live inexplorer/gribwasm/, so no site build needs Rust.spatial:registration: "node".init_time: the latest init found by a bounded probe opens by default. An "Init time" select lists the variable's newest 20 inits. An explicit choice never falls back to another run, and an unwritten slice shows "No data" rather than a blank Ready.lead_timeortime: the slider.ensemble_member: a select labelled with coordinate values, default member 0.ZarrLayerper (variable, block, pinned indices).r32floattexture array, and the step is picked on the GPU._data/explorer.js,catalog-pages.njk):+over;minZoom ≥ 0;wrapLongitudeon the borders.page.route):gribberish+scale_offsetcodec module for the explorer, float64 input, one lead per chunk. Virtual-ref options live where icechunk-js opens the store. Enabled per product in_data/explorer.jsonce the spike proves the templates.minZoom ≤ −1hang; the ECMWF example's registration, if the legacy store confirms it.Out of scope for this phase
Point series, ensemble summaries, animation autoplay, prefetching the next block, and production.
Work plan
Log
2026-09-25 — research and spike
npm view.minZoom;2026-09-25 — Marsh's decisions, build started
2026-09-25 — Track 2 spike passed
scale_offsetis zarrita's built-in codec.ecmwf-forecasts(about 5% of AIFS refs, late 2024 to early 2025) throttles with 503 and no CORS headers, so it needs a retry fetchClient.2026-09-25 — Track 1 on staging; review pass 1
npm run build. Fixed with theeleventy.beforehook, which I verified with a barenpx @11ty/eleventyin a clean worktree. On the clean commit, all unit tests and the 10 offline e2e specs pass. GFS on the live preview: ready, 8.6 MB.npm cion a lockfile change — fixed in a5b11de;Implementer C is fixing 1–4, implementer B is adding tests for 1, 2, 3 and 6, and this entry covers 7.
2026-09-25 — second-look on pass 1; virtual code committed
.node-versionpins Pages to Node 22 (9b17abb).2026-09-25 — all 26 on staging
Committed 0e30809 (advisory GPU estimate, provisional colour range, facade keys and abort) and 2fda2b5 (10 virtual rows, captions, the phone overflow fix, e2e for pass-1 findings 1/2/3/6, and a live virtual spec).
On a clean checkout: 272 unit tests and 66 Playwright specs pass (explorer offline + live, agent-prompt, pipeline).
Staging checked on the preview:
All reached Ready. The wasm and codec chunks are served lazily from Pages.
Virtual per-product results (C, headless Chromium): all 10 render. Registration is 45/45 on each of GFS, HRRR-48, AIFS and GEFS 0.5°. Each lead or time change is one request of 0.14–1.22 MB.
Known limitations:
Blocked: review pass 2 couldn't run. Codex returned
401 Unauthorized: Incorrect API keyon its first request. I reported it to the supervisor. The review is opposite-vendor, so it waits for Codex auth instead of switching to a Claude reviewer. The target stays frozen at 2fda2b5.2026-09-25 — review pass 2
2026-09-25 — review pass 3 (final) and fixes
Retro
What each review pass bought:
Second-look: the highest-leverage calls came before any code was written:
What went wrong:
npm run build. That was learned from a broken first preview rather than checked beforehand.What worked:
Final staging check on e17dbf2's preview (all Ready):
Follow-ups, not blocking:
minZoom ≤ −1hang, the globe-sized tile misregistration).2026-09-26 — CI fix
renderjob failed on e17dbf2. Its e2e web server runs eleventy, whose explorer hook runsnpm ci. Under CI's Node 24 / npm 11, that refused the lockfile as out of sync: it held only the linux-x64-gnu builds of rolldown and lightningcss, and no fsevents.--package-lock-only. That added 25 entries and changed no locked version.npm cipasses with npm 11 and npm 10.render93 passed (all e2e, including explorer offline + live), and unit, CodeQL and Cloudflare Pages pass.2026-09-26 — Marsh's staging feedback, round 2
Marsh: "instead of the example image, show the map empty (no data) with the button overlayed. adding controls for init time and ensemble member (if ensemble) would be cool. What is the 'unload' button for? GEFS seems to fail at any of the 6-hourly steps."
aria-hidden, with the button over it. No explorer bundle and no weather reads before the click.All were fixed in dc4e4de, and E closed two test gaps in bd11232 (a real mid-load level change; an ensemble fixture and member-switch spec).
2026-09-26 — mobile enabled