feat(PORT-004A): add workspace bootstrap and scoped settings
This commit is contained in:
@@ -2,10 +2,12 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from nicegui import ui
|
||||
|
||||
from app.components import PortfolioOverview
|
||||
from app.models.portfolio import get_portfolio_repository
|
||||
from app.models.workspace import WORKSPACE_COOKIE, get_workspace_repository
|
||||
from app.pages.common import dashboard_page, quick_recommendations, recommendation_style, strategy_catalog
|
||||
from app.services.alerts import AlertService, build_portfolio_alert_context
|
||||
from app.services.runtime import get_data_service
|
||||
@@ -31,10 +33,66 @@ def _alert_badge_classes(severity: str) -> str:
|
||||
}.get(severity, "rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-700")
|
||||
|
||||
|
||||
def _render_workspace_recovery(title: str, message: str) -> None:
|
||||
with ui.column().classes("mx-auto mt-24 w-full max-w-2xl gap-6 px-6 text-center"):
|
||||
ui.icon("folder_off").classes("mx-auto text-6xl text-slate-400")
|
||||
ui.label(title).classes("text-3xl font-bold text-slate-900 dark:text-slate-50")
|
||||
ui.label(message).classes("text-base text-slate-500 dark:text-slate-400")
|
||||
with ui.row().classes("mx-auto gap-3"):
|
||||
ui.link("Get started", "/workspaces/bootstrap").classes(
|
||||
"rounded-lg bg-slate-900 px-5 py-3 text-sm font-semibold text-white no-underline dark:bg-slate-100 dark:text-slate-900"
|
||||
)
|
||||
ui.link("Go to welcome page", "/").classes(
|
||||
"rounded-lg border border-slate-300 px-5 py-3 text-sm font-semibold text-slate-700 no-underline dark:border-slate-700 dark:text-slate-200"
|
||||
)
|
||||
|
||||
|
||||
@ui.page("/")
|
||||
def welcome_page(request: Request):
|
||||
repo = get_workspace_repository()
|
||||
workspace_id = request.cookies.get(WORKSPACE_COOKIE, "")
|
||||
if workspace_id and repo.workspace_exists(workspace_id):
|
||||
return RedirectResponse(url=f"/{workspace_id}", status_code=307)
|
||||
|
||||
with ui.column().classes("mx-auto mt-24 w-full max-w-3xl gap-8 px-6"):
|
||||
with ui.card().classes(
|
||||
"w-full rounded-3xl border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
||||
):
|
||||
ui.label("Vault Dashboard").classes("text-sm font-semibold uppercase tracking-[0.2em] text-sky-600")
|
||||
ui.label("Create a private workspace URL").classes("text-4xl font-bold text-slate-900 dark:text-slate-50")
|
||||
ui.label(
|
||||
"Start with a workspace-scoped overview and settings area. Your portfolio defaults are stored server-side and your browser keeps a workspace cookie for quick return visits."
|
||||
).classes("text-base text-slate-500 dark:text-slate-400")
|
||||
with ui.row().classes("items-center gap-4 pt-4"):
|
||||
ui.link("Get started", "/workspaces/bootstrap").classes(
|
||||
"rounded-lg bg-slate-900 px-5 py-3 text-sm font-semibold text-white no-underline dark:bg-slate-100 dark:text-slate-900"
|
||||
)
|
||||
ui.label("You can always create a fresh workspace later if a link is lost.").classes(
|
||||
"text-sm text-slate-500 dark:text-slate-400"
|
||||
)
|
||||
|
||||
|
||||
@ui.page("/overview")
|
||||
async def overview_page() -> None:
|
||||
config = get_portfolio_repository().load()
|
||||
def legacy_overview_page(request: Request):
|
||||
repo = get_workspace_repository()
|
||||
workspace_id = request.cookies.get(WORKSPACE_COOKIE, "")
|
||||
if workspace_id and repo.workspace_exists(workspace_id):
|
||||
return RedirectResponse(url=f"/{workspace_id}", status_code=307)
|
||||
return RedirectResponse(url="/", status_code=307)
|
||||
|
||||
|
||||
@ui.page("/{workspace_id}")
|
||||
@ui.page("/{workspace_id}/overview")
|
||||
async def overview_page(workspace_id: str) -> None:
|
||||
repo = get_workspace_repository()
|
||||
if not repo.workspace_exists(workspace_id):
|
||||
_render_workspace_recovery(
|
||||
"Workspace not found",
|
||||
"The workspace link looks missing or expired. Create a new workspace or return to the welcome page.",
|
||||
)
|
||||
return
|
||||
|
||||
config = repo.load_portfolio_config(workspace_id)
|
||||
data_service = get_data_service()
|
||||
symbol = data_service.default_symbol
|
||||
quote = await data_service.get_quote(symbol)
|
||||
@@ -57,6 +115,7 @@ async def overview_page() -> None:
|
||||
"Overview",
|
||||
"Portfolio health, LTV risk, and quick strategy guidance for the current GLD-backed loan.",
|
||||
"overview",
|
||||
workspace_id=workspace_id,
|
||||
):
|
||||
with ui.row().classes("w-full items-center justify-between gap-4 max-md:flex-col max-md:items-start"):
|
||||
ui.label(quote_status).classes("text-sm text-slate-500 dark:text-slate-400")
|
||||
|
||||
Reference in New Issue
Block a user