feat(studio): live run states over SSE, and a results view in the wizard - #99
Merged
Conversation
Raised in use as three questions -- when does a run finish, where are the results, does the front-end update? The honest answers were "3-5 minutes", "only in /legacy or the API", and "no". Both gaps closed. Live states: the wizard now subscribes to the SSE stream the API has pushed since Phase 0; the rail moves queued -> running -> succeeded with no reload. The first cut carried a lifecycle bug worth recording: the effect's cleanup closed every socket on any change to `runs` -- that is, on the first pushed update -- while the already-watched set prevented reopening, so each stream died the moment it delivered once and the rail froze at "running" while the server had long said "succeeded". Found by driving a real run and comparing against the API's answer. Sockets live in a ref now, closed only on the run's terminal state or unmount. Results: click a run (or submit one -- it self-selects) for headline tiles, flags, and three charts from the RunSummary -- gas phase, total particle number, final size distribution -- read from the summary, never the npz (ADR-004). The first cut invented its own payload shape and rendered four confident zeros: headline scalars live on the RUN row, and the spectrum key is final_size_distribution. The component test's fixture now mirrors the real endpoints, so a drift in either direction fails a test rather than rendering zeros with a straight face. Verified end to end: a 1-day/40-bin run submitted from the review stage went running -> succeeded at t+18 s, tiles filled (1.7e6 pptv final SO2, 15.05 pptv peak H2SO4, 3.1e6 cm^-3 peak N), three charts drew, open_system_dilution flagged. /legacy is now fully superseded (task 1.4); deleting it is its own small PR. 371 Python Tier-A, 63 vitest (+3). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EaDHnPdiacsybr8Tp5WnqR
…six time series, distribution explorer with time slider and log/linear toggle (summary 0.2.0) Review: "the result plots are so bad" and "I do not see any nucleation -- maybe the timestep is the issue." Diagnosed against the stored npz before touching anything: the DATA was fine (dN/dlogDp peaks at 1.85e7 cm^-3 at t = 11.8 h, full 147-step resolution per bin) -- the DISPLAY only ever showed the final spectrum, by which time the burst has grown and coagulated out of the small bins. Not a timestep problem; a "you were only shown the last frame" problem. No model changes. Summary schema 0.1.0 -> 0.2.0: - size_distribution_history: dN/dlogDp on (time x bin), uniformly strided (never a coarsening grid -- CAVEATS records non-uniform resampling aliasing number spikes 8x), capped at 240 samples with the stride declared. - particle_mass_ug_m3: dry H2SO4-equivalent mass from particulate_S, molar mass and Avogadro only (ASSUMPTION-8, pure sulfate) -- a unit conversion, tested against a hand-computed value. - particulate_S_pptv: particle sulfur as a mixing ratio, converted with the SAME air number density as the gas series -- so "H2SO4 in gas and in particles" is one quantity on one axis. The first cut put pptv and molec cm^-3 on a shared log scale, which is the dual-axis lie in disguise; caught on my own screenshot review. The results view, rebuilt to the requested list: - Banana plot: dN/dlogDp over (time x dry Dp), log colour on a paper->navy sequential ramp with a decade-labelled colourbar. Nucleation is the dark tongue in the smallest bins bending up and to the right -- exactly the picture that was missing. - Six time series: total N, particle mass, SO2, H2SO4 gas + particle (pptv), OH + HO2, wet SA. - Number AND surface-area size distributions at a slider-chosen time, log/linear y toggle. dS/dlogDp = pi Dp^2 dN/dlogDp at the DRY diameter -- geometry over served numbers, and the caption says it is not the model's wet SA series. Old summaries (0.1.0) degrade gracefully: final spectrum only, with a note to re-run. Also chased a stray vertical line across full-page screenshots: no matching element exists in the live DOM (probed exhaustively for tall-thin boxes), so it is a captureBeyondViewport tiling artifact of headless Chrome, not a product defect. 373 Python Tier-A (+2), 65 vitest (+2). Verified end to end on a fresh 1-day run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EaDHnPdiacsybr8Tp5WnqR
…arned from the viz pages Review: the results view was bad; learn from d1_globe (now plume_dynamics.html on main) and inverse_lab. Studied both rather than guessing. They carry three things the results view lacked, all derivable from the summary the run already produces -- no model change. Night bands on every time series, from the run's OWN photolysis (daylight = any J > 0), aligned to the stored time grid by interval index. A time search ties at the edges and cannot distinguish the interval a step opens from the one it closes; index alignment is exact because J has one entry per interval, in order. Absent J -> no bands, never a recomputed sun (ADR-005). The OH/HO2 diurnal crash now reads against real day/night. Sulfur budget: a normalized gas-vs-particle stacked area (gold SO2 gas, steel particles), the signature panel of both reference pages. Particle sulfur is already emitted in pptv, so this is a normalization of served series, not new physics. Particle mass series (dry H2SO4-equivalent, ug/m3) from particulate_S via Avogadro and molar mass under ASSUMPTION-8, tested against a hand-computed value. Summary schema is 0.2.0 (the time-resolved spectrum landed in the prior commit; this adds `daylight`). Also fixes a rendering bug I twice wrongly called a screenshot artifact: a full-page vertical blue line. H2SO4 gas starts at exactly 0, and the log y-scale clamped 0 to Number.MIN_VALUE, giving a pixel near -1e308 -- linePath drew a segment off the chart and `overflow: visible` painted it down the page. A zero has no position on a log axis; the scale returns NaN there now and the line breaks like any gap. Two scale tests pin it, and the diagnosis came from elementsFromPoint on the line, not from assuming "capture artifact" -- the lesson being to verify that claim by DOM probe rather than default to it. 375 Python Tier-A, 67 vitest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EaDHnPdiacsybr8Tp5WnqR
aliakherati
force-pushed
the
feat/live-runs-and-results
branch
from
August 27, 2026 05:19
3d48451 to
333652e
Compare
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.
When does it finish, where are the results, does the front-end update?
Asked in review; the honest answers were "3–5 minutes", "only in
/legacyor the API", and"no". This PR makes the answers: the rail tells you, in the wizard, and yes, live.
Live run states (SSE, no reloads)
The API has pushed run states since Phase 0 (
/api/events/runs/{id}); the wizard just neverlistened. Now every non-terminal run in the rail holds one EventSource, and states move
queued → running → succeeded in place.
The first cut carried a lifecycle bug worth recording: the effect's cleanup closed every socket on
any change to
runs— i.e. on the first pushed update — while the already-watched set preventedreopening. Each stream died the moment it delivered once, and the rail froze at running while the
server had long said succeeded. Found by driving a real run and comparing against the API's
answer. Sockets live in a ref now, closed only on terminal state or unmount.
A results view in the wizard
Click a run — or submit one; it self-selects — and get:
log), total particle number, and the final dry size distribution.
the SSE flips it to succeeded.
The first cut invented its own payload shape and rendered four confident zeros — headline
scalars live on the run row, and the spectrum key is
final_size_distribution. The componenttest's fixture now mirrors the real endpoints, so a drift in either direction fails a test instead
of rendering zeros with a straight face.
Verified end to end, in the browser
A 1-day/40-bin run submitted from the review stage:
Real physics on screen: SO₂ decays, H₂SO₄ peaks at 15 pptv, nucleation bursts to 3.1×10⁶ cm⁻³
before coagulation grinds it down.
/legacyis now fully superseded (task 1.4) — deleting it is its own small follow-up PR.371 Python Tier-A, 63 vitest (+3).