From 82e52f716212a8610f32072aa53ade669460cd8e Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 26 Mar 2026 10:32:05 +0100 Subject: [PATCH] fix(UX-001): tighten historical stale state handling --- app/pages/backtests.py | 15 +++++++++--- app/pages/event_comparison.py | 43 +++++++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/app/pages/backtests.py b/app/pages/backtests.py index 5814180..0e69681 100644 --- a/app/pages/backtests.py +++ b/app/pages/backtests.py @@ -290,6 +290,7 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: ).classes("w-full") def refresh_workspace_seeded_units() -> None: + validation_label.set_text("") entry_spot, entry_error = derive_entry_spot() if ( workspace_id @@ -302,11 +303,19 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: else: entry_spot_hint.set_text("Entry spot unavailable until the scenario dates are valid.") render_seeded_summary(entry_spot=entry_spot, entry_spot_error=entry_error) - mark_results_stale() + if entry_error: + render_result_state("Scenario validation failed", entry_error, tone="warning") + else: + mark_results_stale() def on_form_change() -> None: - render_seeded_summary() - mark_results_stale() + validation_label.set_text("") + entry_spot, entry_error = derive_entry_spot() + render_seeded_summary(entry_spot=entry_spot, entry_spot_error=entry_error) + if entry_error: + render_result_state("Scenario validation failed", entry_error, tone="warning") + else: + mark_results_stale() def run_backtest() -> None: validation_label.set_text("") diff --git a/app/pages/event_comparison.py b/app/pages/event_comparison.py index a7d5a45..d0bc8af 100644 --- a/app/pages/event_comparison.py +++ b/app/pages/event_comparison.py @@ -187,13 +187,13 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: tone="info", ) - def refresh_preview(*, reset_templates: bool = False, reseed_units: bool = False) -> None: + def refresh_preview(*, reset_templates: bool = False, reseed_units: bool = False) -> str | None: option = preset_lookup.get(str(preset_select.value or "")) if option is None: metadata_label.set_text("") scenario_label.set_text("") render_selected_summary(entry_spot=None) - return + return None if reset_templates: syncing_controls["value"] = True @@ -202,12 +202,20 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: finally: syncing_controls["value"] = False + template_slugs = selected_template_slugs() + if not template_slugs: + message = "Select at least one strategy template." + metadata_label.set_text(f"Preset: {option['label']} โ€” {option['description']}") + scenario_label.set_text(message) + render_selected_summary(entry_spot=None, entry_spot_error=message) + return message + try: preview_units = float(units_input.value or 0.0) if workspace_id and config is not None and reseed_units: preview_scenario = service.preview_scenario( preset_slug=str(option["slug"]), - template_slugs=selected_template_slugs(), + template_slugs=template_slugs, underlying_units=1.0, loan_amount=float(loan_input.value or 0.0), margin_call_ltv=float(ltv_input.value or 0.0), @@ -224,7 +232,7 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: syncing_controls["value"] = False scenario = service.preview_scenario( preset_slug=str(option["slug"]), - template_slugs=selected_template_slugs(), + template_slugs=template_slugs, underlying_units=preview_units, loan_amount=float(loan_input.value or 0.0), margin_call_ltv=float(ltv_input.value or 0.0), @@ -233,7 +241,7 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: metadata_label.set_text(f"Preset: {option['label']} โ€” {option['description']}") scenario_label.set_text(str(exc)) render_selected_summary(entry_spot=None, entry_spot_error=str(exc)) - return + return str(exc) preset = service.event_preset_service.get_preset(str(option["slug"])) metadata_label.set_text(f"Preset: {option['label']} โ€” {option['description']}") scenario_label.set_text( @@ -247,14 +255,21 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: + f" ยท Entry spot: ${scenario.initial_portfolio.entry_spot:,.2f}" ) render_selected_summary(entry_spot=float(scenario.initial_portfolio.entry_spot)) + return None def render_report() -> None: validation_label.set_text("") result_panel.clear() + template_slugs = selected_template_slugs() + if not template_slugs: + message = "Select at least one strategy template." + validation_label.set_text(message) + render_result_state("Scenario validation failed", message, tone="warning") + return try: report = service.run_read_only_comparison( preset_slug=str(preset_select.value or ""), - template_slugs=selected_template_slugs(), + template_slugs=template_slugs, underlying_units=float(units_input.value or 0.0), loan_amount=float(loan_input.value or 0.0), margin_call_ltv=float(ltv_input.value or 0.0), @@ -388,14 +403,22 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: def on_preset_change() -> None: if syncing_controls["value"]: return - refresh_preview(reset_templates=True, reseed_units=True) - mark_results_stale() + validation_label.set_text("") + preview_error = refresh_preview(reset_templates=True, reseed_units=True) + if preview_error: + render_result_state("Scenario validation failed", preview_error, tone="warning") + else: + mark_results_stale() def on_preview_input_change() -> None: if syncing_controls["value"]: return - refresh_preview() - mark_results_stale() + validation_label.set_text("") + preview_error = refresh_preview() + if preview_error: + render_result_state("Scenario validation failed", preview_error, tone="warning") + else: + mark_results_stale() preset_select.on_value_change(lambda _: on_preset_change()) template_select.on_value_change(lambda _: on_preview_input_change())