Saved runs: the last report section, plus the closing disclaimer - #17
Merged
Merged
Conversation
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
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>
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.
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:
"{n} assets · {period}") so accepting the default is a single Enter.markowizard.savedRuns: reload parameters (sel/period/rfIdx) plus the raw (pre-cash) selected-portfolio figures at save time. Reads/writes wrapped intry/catch— private browsing or a full quota degrades to "didn't persist" rather than breaking the report.sel/period/rfIdxfrom 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."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
localStoragewith the correct reload parameters; loading a saved run triggers a fresh/api/analyzecall 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