fix(UX-001): address layout review findings

This commit is contained in:
Bu5hm4nn
2026-03-26 10:24:52 +01:00
parent a60c5fb1f2
commit 78de8782c4
7 changed files with 238 additions and 98 deletions

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
import logging
from fastapi.responses import RedirectResponse
from nicegui import ui
@@ -8,6 +10,8 @@ from app.models.workspace import get_workspace_repository
from app.pages.common import dashboard_page, split_page_panes
from app.services.event_comparison_ui import EventComparisonPageService
logger = logging.getLogger(__name__)
def _chart_options(dates: tuple[str, ...], series: tuple[dict[str, object], ...]) -> dict:
return {
@@ -124,13 +128,15 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
with right_pane:
result_panel = ui.column().classes("w-full gap-6")
syncing_controls = {"value": False}
def selected_template_slugs() -> tuple[str, ...]:
raw_value = template_select.value or []
if isinstance(raw_value, str):
return (raw_value,) if raw_value else ()
return tuple(str(item) for item in raw_value if item)
def render_selected_summary(entry_spot: float | None = None) -> None:
def render_selected_summary(entry_spot: float | None = None, entry_spot_error: str | None = None) -> None:
selected_summary.clear()
with selected_summary:
ui.label("Scenario Summary").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
@@ -144,7 +150,7 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
("Underlying units", f"{float(units_input.value or 0.0):,.0f}"),
("Loan amount", f"${float(loan_input.value or 0.0):,.0f}"),
("Margin call LTV", f"{float(ltv_input.value or 0.0):.1%}"),
("Entry spot", f"${entry_spot:,.2f}" if entry_spot is not None else "Pending"),
("Entry spot", f"${entry_spot:,.2f}" if entry_spot is not None else "Unavailable"),
]
for label, value in cards:
with ui.card().classes(
@@ -152,18 +158,53 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
):
ui.label(label).classes("text-sm text-slate-500 dark:text-slate-400")
ui.label(value).classes("text-xl font-bold text-slate-900 dark:text-slate-100")
if entry_spot_error:
ui.label(entry_spot_error).classes("text-sm text-amber-700 dark:text-amber-300")
def refresh_preset_details() -> None:
def render_result_state(title: str, message: str, *, tone: str = "info") -> None:
tone_classes = {
"info": "border-sky-200 bg-sky-50 dark:border-sky-900/60 dark:bg-sky-950/30",
"warning": "border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30",
"error": "border-rose-200 bg-rose-50 dark:border-rose-900/60 dark:bg-rose-950/30",
}
text_classes = {
"info": "text-sky-800 dark:text-sky-200",
"warning": "text-amber-800 dark:text-amber-200",
"error": "text-rose-800 dark:text-rose-200",
}
result_panel.clear()
with result_panel:
with ui.card().classes(
f"w-full rounded-2xl border shadow-sm {tone_classes.get(tone, tone_classes['info'])}"
):
ui.label(title).classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
ui.label(message).classes(f"text-sm {text_classes.get(tone, text_classes['info'])}")
def mark_results_stale() -> None:
render_result_state(
"Results out of date",
"Inputs changed. Run comparison again to refresh rankings and portfolio paths for the current scenario.",
tone="info",
)
def refresh_preview(*, reset_templates: bool = False, reseed_units: bool = False) -> 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
template_select.value = list(service.default_template_selection(str(option["slug"])))
if reset_templates:
syncing_controls["value"] = True
try:
template_select.value = list(service.default_template_selection(str(option["slug"])))
finally:
syncing_controls["value"] = False
try:
preview_units = float(units_input.value or 0.0)
if workspace_id and config is not None:
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(),
@@ -176,7 +217,11 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
entry_spot=float(preview_scenario.initial_portfolio.entry_spot),
symbol="GLD",
)
units_input.value = preview_units
syncing_controls["value"] = True
try:
units_input.value = preview_units
finally:
syncing_controls["value"] = False
scenario = service.preview_scenario(
preset_slug=str(option["slug"]),
template_slugs=selected_template_slugs(),
@@ -187,7 +232,7 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
except (ValueError, KeyError) as exc:
metadata_label.set_text(f"Preset: {option['label']}{option['description']}")
scenario_label.set_text(str(exc))
render_selected_summary(entry_spot=None)
render_selected_summary(entry_spot=None, entry_spot_error=str(exc))
return
preset = service.event_preset_service.get_preset(str(option["slug"]))
metadata_label.set_text(f"Preset: {option['label']}{option['description']}")
@@ -203,15 +248,6 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
)
render_selected_summary(entry_spot=float(scenario.initial_portfolio.entry_spot))
def render_result_validation(message: str) -> None:
result_panel.clear()
with result_panel:
with ui.card().classes(
"w-full rounded-2xl border border-amber-200 bg-amber-50 shadow-sm dark:border-amber-900/60 dark:bg-amber-950/30"
):
ui.label("Scenario Results").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
ui.label(message).classes("text-sm text-amber-800 dark:text-amber-200")
def render_report() -> None:
validation_label.set_text("")
result_panel.clear()
@@ -225,12 +261,21 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
)
except (ValueError, KeyError) as exc:
validation_label.set_text(str(exc))
render_result_validation(str(exc))
render_result_state("Scenario validation failed", str(exc), tone="warning")
return
except Exception:
message = "Event comparison failed. Please verify the seeded inputs and try again."
logger.exception(
"Event comparison page run failed for workspace=%s preset=%s templates=%s units=%s loan=%s margin_call_ltv=%s",
workspace_id,
preset_select.value,
selected_template_slugs(),
units_input.value,
loan_input.value,
ltv_input.value,
)
validation_label.set_text(message)
render_result_validation(message)
render_result_state("Event comparison failed", message, tone="error")
return
preset = report.event_preset
@@ -340,11 +385,23 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
)
).classes("h-96 w-full")
preset_select.on_value_change(lambda _: refresh_preset_details())
template_select.on_value_change(lambda _: refresh_preset_details())
units_input.on_value_change(lambda _: refresh_preset_details())
loan_input.on_value_change(lambda _: refresh_preset_details())
ltv_input.on_value_change(lambda _: refresh_preset_details())
def on_preset_change() -> None:
if syncing_controls["value"]:
return
refresh_preview(reset_templates=True, reseed_units=True)
mark_results_stale()
def on_preview_input_change() -> None:
if syncing_controls["value"]:
return
refresh_preview()
mark_results_stale()
preset_select.on_value_change(lambda _: on_preset_change())
template_select.on_value_change(lambda _: on_preview_input_change())
units_input.on_value_change(lambda _: on_preview_input_change())
loan_input.on_value_change(lambda _: on_preview_input_change())
ltv_input.on_value_change(lambda _: on_preview_input_change())
run_button.on_click(lambda: render_report())
refresh_preset_details()
refresh_preview(reset_templates=False, reseed_units=False)
render_report()