feat(hub): dashboard SSE push, light-theme cards, simplify AI coach

Replace dashboard polling with backend SSE and snapshot refresh. Restyle for light/dark theme with soft card glow instead of neon. Remove Today's Summary from AI page; keep trading and general chat only. Update hub documentation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-11 10:53:50 +08:00
parent 582ada7e60
commit 07e8604ea6
11 changed files with 481 additions and 424 deletions
+52 -2
View File
@@ -90,6 +90,8 @@ from url_public import browser_url, default_review_url, public_origin
from urllib.parse import urlencode
from hub_board_cache import HUB_BOARD_POLL_INTERVAL, board_store
from hub_dashboard_cache import dashboard_store
from hub_dashboard import DASHBOARD_POLL_INTERVAL_SEC
from hub_chart_cache import (
HUB_CHART_POLL_INTERVAL,
HUB_CHART_WATCH_TTL_SEC,
@@ -271,6 +273,7 @@ async def _run_board_aggregate() -> dict:
def _schedule_board_refresh() -> None:
board_store.request_refresh()
dashboard_store.request_refresh()
async def _run_archive_sync_once() -> dict:
@@ -470,6 +473,7 @@ async def _archive_sync_loop() -> None:
async def _hub_lifespan(_app: FastAPI):
global _archive_sync_stop, _archive_sync_task, _volume_rank_stop, _volume_rank_task
await board_store.start(_run_board_aggregate)
await dashboard_store.start(_run_dashboard_aggregate)
await chart_poll_store.start(_run_chart_poll)
_archive_sync_stop = asyncio.Event()
_archive_sync_task = asyncio.create_task(_archive_sync_loop(), name="hub-archive-sync")
@@ -499,6 +503,7 @@ async def _hub_lifespan(_app: FastAPI):
_volume_rank_task = None
_volume_rank_stop = None
await chart_poll_store.stop()
await dashboard_store.stop()
await board_store.stop()
@@ -667,9 +672,26 @@ from hub_dashboard import build_dashboard_payload, default_trading_day
app.include_router(create_hub_ai_router(load_all_exchanges=_all_exchanges_for_ai))
async def _run_dashboard_aggregate() -> dict:
try:
return await asyncio.to_thread(
build_dashboard_payload,
_all_exchanges_for_ai(),
trading_day=default_trading_day(),
)
except Exception as exc:
return {"ok": False, "msg": str(exc), "error": "aggregate_failed"}
def _schedule_dashboard_refresh() -> None:
dashboard_store.request_refresh()
@app.get("/api/dashboard/daily")
def api_dashboard_daily(trading_day: str = ""):
day = (trading_day or "").strip()[:10] or default_trading_day()
if not (trading_day or "").strip():
return dashboard_store.snapshot_dict()
try:
payload = build_dashboard_payload(
_all_exchanges_for_ai(),
@@ -677,7 +699,28 @@ def api_dashboard_daily(trading_day: str = ""):
)
except Exception as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return payload
return {**payload, "dashboard_version": dashboard_store.version}
@app.get("/api/dashboard/stream")
async def api_dashboard_stream():
from fastapi.responses import StreamingResponse
return StreamingResponse(
dashboard_store.iter_sse(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
@app.post("/api/dashboard/refresh")
async def api_dashboard_refresh():
_schedule_dashboard_refresh()
return {"ok": True, "dashboard_version": dashboard_store.version}
@app.get("/trade")
@@ -2122,7 +2165,7 @@ def api_ping():
"service": "manual-trading-hub",
"build": HUB_BUILD,
"trade_ui": False,
"features": ["monitor", "settings", "auth", "board_sse", "archive", "dashboard", "funds"],
"features": ["monitor", "settings", "auth", "board_sse", "dashboard_sse", "archive", "dashboard", "funds"],
"board_poll_interval_sec": HUB_BOARD_POLL_INTERVAL,
"board_version": board_store.version,
"board_aggregating": board_store.aggregating,
@@ -2130,6 +2173,13 @@ def api_ping():
if isinstance(board_store.payload, dict)
else None,
"board_error": board_store.last_error,
"dashboard_poll_interval_sec": DASHBOARD_POLL_INTERVAL_SEC,
"dashboard_version": dashboard_store.version,
"dashboard_aggregating": dashboard_store.aggregating,
"dashboard_updated_at": (dashboard_store.payload or {}).get("updated_at")
if isinstance(dashboard_store.payload, dict)
else None,
"dashboard_error": dashboard_store.last_error,
"password_required": password_required(),
"env_disabled_ids": sorted(env_force_disabled_ids()),
"hub_disabled_ids_raw": (os.getenv("HUB_DISABLED_IDS") or ""),