Skip to content

Saved runs: the last report section, plus the closing disclaimer - #17

Merged
GusFurtado merged 1 commit into
correlation-assets-sectionsfrom
saved-runs
Sep 11, 2026
Merged

GusFurtado merged 1 commit into
correlation-assets-sectionsfrom
saved-runs

Conversation

@GusFurtado

Copy link
Copy Markdown
Collaborator

Context

PR 7 (last one) of the widescreen "analytical workstation" report redesign. Stacked on #16 (targets that branch, not main).

What's here

This is the section the handoff explicitly leaves undesigned: "saving is not implemented — the three rows are fixtures... see Gaps." Per the earlier agreed decision, v1 persists to this browser's localStorage — no backend changes, no new endpoints.

Everything below is this project's own invention, not from the handoff:

  • A "Save this run" button (disabled until a run has succeeded), prompting for a name pre-filled with a sensible default ("{n} assets · {period}") so accepting the default is a single Enter.
  • Storage as a JSON array under markowizard.savedRuns: reload parameters (sel/period/rfIdx) plus the raw (pre-cash) selected-portfolio figures at save time. Reads/writes wrapped in try/catch — private browsing or a full quota degrades to "didn't persist" rather than breaking the report.
  • Load replaces sel/period/rfIdx from the saved entry (discarding any unrelated pending rail edits) and re-runs the analysis — matches the handoff's "loading a run replaces the universe, window and selection."
  • Delete, and an empty state for first-time users — neither specified, but both necessary for this to be a usable feature rather than a write-only list.

The saved-runs list isn't gated on the current result the way every other section is: it stays visible and usable (Load included) even after a failed re-run, so a bad refresh doesn't strand the user away from a previously-good report.

Also adds the closing disclaimer paragraph from the handoff's own "Disclaimer" spec — the one static piece of the report not yet in the tree.

With this PR, every section in the layout shell (#12) is built. The widescreen report redesign is complete end to end, across this 7-PR stack (#11#17).

Verification

Headless Chromium: save (with a custom name) persists to localStorage with the correct reload parameters; loading a saved run triggers a fresh /api/analyze call with that run's tickers/period even when the rail's pending controls had since been changed to something else; delete and the empty state both render correctly; cancelling the name prompt saves nothing. Also checked the sticky header doesn't actually duplicate mid-page — that was a full-page-screenshot stitching artifact from headless Chromium, confirmed by a normal scrolled viewport screenshot instead.

🤖 Generated with Claude Code

This is the section the handoff explicitly leaves undesigned ("saving
is not implemented — the three rows are fixtures... see Gaps"). Per
the earlier agreed decision, v1 persists to this browser's
localStorage — no backend changes, no new endpoints.

Everything in the "Saved runs" comment block in frontend/app.js is
this project's own invention, not from the handoff:
- A "Save this run" button (disabled until a run has succeeded),
  prompting for a name pre-filled with a sensible default (`"{n}
  assets · {period}"`) so accepting the default is a single Enter.
- Storage as a JSON array under `markowizard.savedRuns`, one entry per
  save: the reload parameters (sel/period/rfIdx) plus the raw
  (pre-cash) selected-portfolio figures at save time. Reads/writes are
  wrapped in try/catch — private browsing or a full quota degrades to
  "didn't persist" rather than breaking the report.
- Load replaces sel/period/rfIdx from the saved entry (discarding any
  unrelated pending edits in the rail) and re-runs the analysis, same
  as the handoff describes ("loading a run replaces the universe,
  window and selection").
- Delete, and an empty state for first-time users — neither specified,
  but both necessary for this to be a usable feature rather than a
  write-only list.

The saved-runs list itself isn't gated on the current result the way
every other section is: it stays visible and usable (Load included)
even after a failed re-run, so a bad refresh doesn't strand the user
away from a previously-good report.

Also adds the closing disclaimer paragraph from the handoff's
"Disclaimer" spec, the one static piece of the report not yet in the
tree. With this PR every section in the layout shell (#12) is now
built: the widescreen report redesign is complete end to end.

Verified with headless Chromium: save (with a custom name) persists to
localStorage with the correct reload parameters; loading a saved run
triggers a fresh /api/analyze call with *that run's* tickers/period
even when the rail's pending controls had since been changed to
something else; delete and the empty state both render correctly; and
cancelling the name prompt saves nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@GusFurtado
GusFurtado merged commit a912fef into correlation-assets-sections Sep 11, 2026
5 checks passed
@GusFurtado
GusFurtado deleted the saved-runs branch September 11, 2026 23:23
GusFurtado added a commit that referenced this pull request Sep 11, 2026
…ck (#18)

* Wire up the control rail and KPI band against the real API

frontend/app.js (new): a pending/applied state machine — sel, period
and rfIdx are what the rail shows; appliedSel/appliedPeriod/appliedRfIdx
are what the current result was actually solved from. They start equal
(the app auto-runs once on load with the default universe, so there's
always a report on screen rather than an empty first-load state) and
diverge the moment a control changes, which is what flips the run
button between "Run analysis" and "Re-run analysis".

Wired up:
- Universe chips (toggle, minimum-2 floor enforced as a no-op below it)
- History window segment + risk-free slider (live-updating display)
- Run -> POST /api/analyze, running overlay, and an error card with a
  Retry action for 400/500s (no design exists for this yet — see the
  handoff's own Gaps list — so this reuses the card primitive plus the
  semantic error color rather than inventing new visual language)
- Header tags/status/units toggle, reflecting the applied run once one
  exists
- The KPI band: live headline/lede and the 5 KPI cards, monthly/annual
  unit conversion threaded through all of them

The frontier selection (iF) and cash blend aren't wired yet — both
default to "no selection" (tangency, 0% cash), so the KPI band always
reads max_sharpe_portfolio for now. Both land with their own sections
in upcoming PRs and this file already reads through that same state
shape so they slot in without a rework.

Also: .mw-overlay's own `display: grid` outranked the UA stylesheet's
`[hidden]{display:none}` at equal specificity (source order), so a
"hidden" overlay still intercepted clicks — found by a headless-browser
click test, fixed with an explicit `.mw-overlay[hidden]{display:none}`.

Verified with a headless Chromium against a mocked /api/analyze
(matching how tests/test_api.py mocks fetch_prices): happy path KPI
values, monthly<->annual conversion, the chip minimum-2 floor, and the
error/retry path all checked by script, plus visual screenshots.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Efficient frontier: SVG chart with drag selection, per-asset dots

The signature interaction of the whole redesign: an SVG chart in
absolute risk/return space (both axes start at zero, not min-max
normalized) with the frontier curve, single-asset points, the capital
allocation line, minimum-variance/tangency markers, and a selection
that tracks pointer drag 1:1, plus a stats panel that follows it.

frontend/app.js:
- resolveSelection(): the shared "which frontier point is selected, and
  what do we call it" logic, used by both the frontier stats panel and
  the KPI headline (which no longer hardcodes the tangency copy — it
  now shows the same position-relative label the frontier does when a
  non-default point is selected).
- computeFrontierGeometry(): scale functions and point coordinates,
  derived from the real /api/analyze response — no client-side solver,
  per the handoff ("do not port the prototype's solver").
- Base vs. selection rendering split: the SVG and its static layers
  (grid, ticks, frontier curve, CAL, per-asset dots, min-var/tangency
  markers) are only rebuilt when the result or the units changes;
  dragging only updates the crosshair/marker attributes and the stats
  panel text. This matters because pointerdown captures the pointer on
  the SVG element itself — rebuilding it mid-drag (e.g. from an
  innerHTML replace on every pointermove) would silently drop that
  capture and break the gesture.
- pickFrontierPoint(): maps a pointer x-position to the nearest
  frontier point by risk, accounting for the plot box's left inset
  rather than a naive fraction of element width — same as the handoff.

Two real bugs found by headless-browser testing, not by inspection:
- Toggling units (monthly/annual) didn't update the chart's axis
  titles or tick labels, only the stats panel — because those are part
  of the "base" HTML, which is keyed only on the result reference, and
  toggling units doesn't change that reference. Fixed by keying the
  base rebuild on (result, units) together. Safe to do because a units
  toggle is a discrete click, never a pointermove mid-drag.
- (Carried a class of risk in from PR 3, re-confirmed here): any bug
  that forces a full base rebuild during a drag would drop pointer
  capture. Verified this isn't happening by dragging, then toggling
  units, and confirming the selection survives the rebuild unchanged.

Verified with headless Chromium against a synthetic /api/analyze
response shaped like a real efficient frontier (concave, tangency
mid-curve): mouse-drag selection, the Min variance / Max Sharpe jump
buttons, and the monthly/annual toggle, each checked against the
rendered DOM plus screenshots.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Optimal allocation: donut, cash blend, and the two-frame weights table

frontend/app.js:
- The donut (stroke-dasharray arcs + an HTML center overlay — SVG <text>
  at this weight shapes to zero width, per the handoff), the cash-blend
  slider, and a weights table that keeps two frames of the same numbers
  clearly separate: "in risky mix" (sums to 100%) and "of capital"
  (sums to 1 - cash) — a mixing-up of these two was called out in the
  handoff as a real review bug on this project, so both the percentage
  *and* the bar-fill width for a row always come from the same frame.
- cashBlend(): a plain weighted average of the selected portfolio and
  cash. This is a deliberate departure from the handoff's own
  suggestion to prefer the API's capital_allocation_line[] — that array
  is anchored to the tangency portfolio specifically (that's what makes
  it a capital allocation line: blending cash with any other point
  doesn't dominate the frontier). Now that PR 4 lets the frontier
  selection move off tangency, the API array can't answer "blend cash
  with *this* selected point" for a non-tangency selection — but the
  handoff's own interactive prototype already does exactly this local
  blend for whatever point is selected, so this follows the prototype's
  actual behavior over the README's simplifying prose.
- Fixed a real inconsistency this surfaced: the KPI band's Expected
  return/Volatility are supposed to be the cash-blended figures (only
  Sharpe is unaffected by cash, per its own "unchanged by the cash
  blend" note) — PR 3 had them reading the raw portfolio figures
  instead, which happened to be harmless while cash was always 0. Fixed
  now that cash is live, using the same cashBlend() helper.
- Same base/update split as the frontier chart, for the same reason:
  the cash `<input type=range>` fires `input` continuously while being
  dragged, and rebuilding it mid-drag (e.g. from an innerHTML replace
  on every tick) would interrupt the browser's own drag gesture on it.
  Only the row identity (tickers/names/colors) is "base"; every number,
  bar width, and the donut are refreshed on every render without
  touching the slider element itself.

frontend/style.css: adds the shared "not a <table>" grid-row primitives
(.mw-table, .mw-grid-row, .mw-swatch, .mw-bar-track/.mw-bar-fill) that
the correlation, per-asset-statistics and saved-runs sections will
reuse in upcoming PRs, plus the allocation-specific layout.

Verified with headless Chromium against a synthetic /api/analyze
response: donut/table/KPI band agree at cash=0% and cash=50% (checked
the exact blended numbers, not just that something rendered), the
cash-row and per-asset rows sum correctly in both frames, and blending
cash off a dragged (non-tangency) frontier selection reads that
selection's own figures rather than silently falling back to tangency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Correlation matrix and per-asset statistics

Both bundled in one PR since they share the grid-row table pattern
PR 5 already built (.mw-table/.mw-grid-row/.mw-num), and neither has
any interactive state of its own — no drag, no slider — so both are
plain full-rebuild-on-render sections, unlike frontier/allocation.

frontend/app.js:
- correlationSectionHtml(): a CSS-grid matrix (not a <table>, matching
  the rest of the report), cell color linearly interpolated in RGB from
  neutral-900 to accent-600. Text stays ink-dark at every value — this
  ramp (unlike the mobile design's) never needs a light/dark contrast
  threshold, since it tops out light enough that dark text stays
  correct throughout. The least/most-correlated-pair callout scans only
  the upper triangle so each pair is considered once.
- assetStatsSectionHtml(): standalone per-asset expected return/
  volatility (from asset_statistics — independent of any portfolio),
  a client-computed standalone Sharpe, and the Weight column reads
  through selectedPortfolio() — so, matching the handoff, it updates
  live as the frontier selection is dragged, not just at the tangency
  default. Zero/near-zero weights dim to neutral-600 rather than being
  hidden, so an asset the optimizer declined stays visible.

Verified with headless Chromium against a synthetic /api/analyze
response: the matrix diagonal is 1.00 everywhere, the least/most
correlated pairs match a manual scan of the input matrix, and the
per-asset Weight column changes when jumping to the minimum-variance
point on the frontier (confirming it tracks selection, not a fixed
snapshot) — plus a full-page screenshot checked against the handoff.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Saved runs: the last report section, plus the closing disclaimer (#17)

This is the section the handoff explicitly leaves undesigned ("saving
is not implemented — the three rows are fixtures... see Gaps"). Per
the earlier agreed decision, v1 persists to this browser's
localStorage — no backend changes, no new endpoints.

Everything in the "Saved runs" comment block in frontend/app.js is
this project's own invention, not from the handoff:
- A "Save this run" button (disabled until a run has succeeded),
  prompting for a name pre-filled with a sensible default (`"{n}
  assets · {period}"`) so accepting the default is a single Enter.
- Storage as a JSON array under `markowizard.savedRuns`, one entry per
  save: the reload parameters (sel/period/rfIdx) plus the raw
  (pre-cash) selected-portfolio figures at save time. Reads/writes are
  wrapped in try/catch — private browsing or a full quota degrades to
  "didn't persist" rather than breaking the report.
- Load replaces sel/period/rfIdx from the saved entry (discarding any
  unrelated pending edits in the rail) and re-runs the analysis, same
  as the handoff describes ("loading a run replaces the universe,
  window and selection").
- Delete, and an empty state for first-time users — neither specified,
  but both necessary for this to be a usable feature rather than a
  write-only list.

The saved-runs list itself isn't gated on the current result the way
every other section is: it stays visible and usable (Load included)
even after a failed re-run, so a bad refresh doesn't strand the user
away from a previously-good report.

Also adds the closing disclaimer paragraph from the handoff's
"Disclaimer" spec, the one static piece of the report not yet in the
tree. With this PR every section in the layout shell (#12) is now
built: the widescreen report redesign is complete end to end.

Verified with headless Chromium: save (with a custom name) persists to
localStorage with the correct reload parameters; loading a saved run
triggers a fresh /api/analyze call with *that run's* tickers/period
even when the rail's pending controls had since been changed to
something else; delete and the empty state both render correctly; and
cancelling the name prompt saves nothing.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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