Skip to content

feat(studio): live run states over SSE, and a results view in the wizard - #99

Merged
aliakherati merged 3 commits into
studio/devfrom
feat/live-runs-and-results
Aug 27, 2026
Merged

feat(studio): live run states over SSE, and a results view in the wizard#99
aliakherati merged 3 commits into
studio/devfrom
feat/live-runs-and-results

Conversation

@aliakherati

Copy link
Copy Markdown
Collaborator

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 /legacy or 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 never
listened. 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 prevented
reopening. 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:

  • Headline tiles: final SO₂, peak H₂SO₄, peak particle number, final SA (wet), plus flags.
  • Three charts from the RunSummary (never the npz — ADR-004): gas phase (SO₂/H₂SO₄/OH, pptv,
    log), total particle number, and the final dry size distribution.
  • Honest empty states: a running run says "no summary yet — the run is running" and fills in when
    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 component
test'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:

t=+ 0s   rail state -> running        (no reload, SSE)
t=+18s   rail state -> succeeded
tiles:   1.7e+6 pptv | 15.05 pptv | 3.1e+6 cm⁻³ | 211.6 µm² cm⁻³
charts:  3 (gas phase, particles, final size distribution)
flags:   open_system_dilution

Real physics on screen: SO₂ decays, H₂SO₄ peaks at 15 pptv, nucleation bursts to 3.1×10⁶ cm⁻³
before coagulation grinds it down.

/legacy is now fully superseded (task 1.4) — deleting it is its own small follow-up PR.

371 Python Tier-A, 63 vitest (+3).

Stacked on #98#97. Merge bottom-up; rebase on request as usual.

aliakherati and others added 3 commits August 26, 2026 22:17
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
aliakherati force-pushed the feat/live-runs-and-results branch from 3d48451 to 333652e Compare August 27, 2026 05:19
@aliakherati
aliakherati merged commit 888e5ff into studio/dev Aug 27, 2026
2 checks passed
@aliakherati
aliakherati deleted the feat/live-runs-and-results branch August 27, 2026 05:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant