diff --git a/backend/app/api/system.py b/backend/app/api/system.py index e51d831..1c7dab5 100644 --- a/backend/app/api/system.py +++ b/backend/app/api/system.py @@ -11,6 +11,8 @@ from app.crypto_agent.crypto_agent import get_crypto_agent from app.services.signal_database_service import get_signal_db_service from app.services.paper_trading_service import get_paper_trading_service from app.services.bitget_live_trading_service import get_all_bitget_live_services, get_bitget_live_service +from app.services.price_monitor_service import get_price_monitor_service +from app.services.runtime_status_service import get_runtime_status router = APIRouter() @@ -429,6 +431,11 @@ async def get_console_snapshot(): if (_parse_signal_timestamp(signal.get("created_at")) or datetime.min) >= recent_cutoff ) + price_monitor = get_price_monitor_service() + configured_symbols = [symbol.strip().upper() for symbol in (get_settings().crypto_symbols or "").split(",") if symbol.strip()] + for symbol in configured_symbols: + price_monitor.subscribe_symbol(symbol) + paper_position_items = [ _normalize_platform_position("paper", pos) for pos in paper_service.get_open_positions()[:12] @@ -517,6 +524,16 @@ async def get_console_snapshot(): "recent_30m_count": recent_signal_count, }, "platforms": platforms_payload, + "monitoring": { + "price_monitor": { + "running": price_monitor.is_running(), + "mode": "websocket" if getattr(price_monitor, "_use_websocket", False) else "polling", + "subscribed_symbols": price_monitor.get_subscribed_symbols(), + "latest_prices": price_monitor.get_all_prices(), + "checked_at": now.isoformat(), + }, + "execution_loop": get_runtime_status("price_monitor_loop"), + }, "management": { "positions": unified_positions[:18], "orders": unified_orders[:24], diff --git a/backend/app/main.py b/backend/app/main.py index 09c31a8..f13a408 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -5,7 +5,7 @@ import asyncio from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, RedirectResponse from contextlib import asynccontextmanager from app.config import get_settings from app.utils.logger import logger @@ -485,7 +485,7 @@ if os.path.exists(frontend_path): @app.get("/") async def root(): """根路径,返回主应用页面""" - index_path = os.path.join(frontend_path, "trading.html") + index_path = os.path.join(frontend_path, "console.html") if os.path.exists(index_path): return FileResponse(index_path) return {"message": "页面不存在"} @@ -493,7 +493,7 @@ async def root(): @app.get("/app") async def app_page(): """主应用页面""" - index_path = os.path.join(frontend_path, "trading.html") + index_path = os.path.join(frontend_path, "console.html") if os.path.exists(index_path): return FileResponse(index_path) return {"message": "页面不存在"} @@ -505,19 +505,13 @@ async def health_check(): @app.get("/trading") async def trading_page(): - """交易页面""" - page_path = os.path.join(frontend_path, "trading.html") - if os.path.exists(page_path): - return FileResponse(page_path) - return {"message": "页面不存在"} + """交易页面兼容入口,统一跳转到总控台""" + return RedirectResponse(url="/console", status_code=307) @app.get("/bitget-trading") async def bitget_trading_page(): - """Bitget 交易页面兼容入口,统一跳转到当前 trading 页面""" - page_path = os.path.join(frontend_path, "trading.html") - if os.path.exists(page_path): - return FileResponse(page_path) - return {"message": "页面不存在"} + """Bitget 交易页面兼容入口,统一跳转到总控台""" + return RedirectResponse(url="/console", status_code=307) @app.get("/signals") async def signals_page(): diff --git a/frontend/console.html b/frontend/console.html index 49e93b4..a94a1a7 100644 --- a/frontend/console.html +++ b/frontend/console.html @@ -58,6 +58,167 @@ padding: 24px 0 40px; } + .console-hub { + display: grid; + grid-template-columns: 240px minmax(0, 1fr); + gap: 18px; + align-items: start; + } + + .console-sidebar { + position: sticky; + top: 18px; + padding: 16px; + border-radius: 22px; + background: var(--panel); + border: 1px solid var(--line); + box-shadow: var(--shadow); + backdrop-filter: blur(18px); + } + + .console-sidebar::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + border-radius: 22px; + background: linear-gradient(180deg, rgba(255,255,255,0.04), transparent 24%, transparent 76%, rgba(126, 200, 255, 0.04)); + } + + .sidebar-label { + color: var(--muted); + font-size: 11px; + margin-bottom: 12px; + font-family: "IBM Plex Mono", monospace; + text-transform: uppercase; + letter-spacing: 0.08em; + } + + .sidebar-nav { + display: grid; + gap: 10px; + } + + .sidebar-link { + appearance: none; + width: 100%; + text-align: left; + cursor: pointer; + padding: 14px 16px; + border-radius: 16px; + border: 1px solid rgba(255,255,255,0.06); + background: rgba(255,255,255,0.03); + color: var(--text); + transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease; + } + + .sidebar-link:hover { + transform: translateY(-1px); + border-color: rgba(126, 200, 255, 0.18); + } + + .sidebar-link.active { + border-color: rgba(126, 200, 255, 0.28); + background: rgba(126, 200, 255, 0.10); + } + + .sidebar-link-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + font-size: 14px; + font-weight: 600; + margin-bottom: 6px; + } + + .sidebar-link-sub { + color: var(--muted); + font-size: 12px; + line-height: 1.55; + } + + .sidebar-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 24px; + height: 20px; + padding: 0 8px; + border-radius: 999px; + background: rgba(255,255,255,0.08); + color: var(--cold); + font-size: 10px; + font-family: "IBM Plex Mono", monospace; + } + + .hub-content { + display: grid; + gap: 18px; + } + + .hub-pane { + display: none; + gap: 18px; + } + + .hub-pane.active { + display: grid; + } + + .pane-grid { + display: grid; + gap: 18px; + } + + .pane-grid.two-col { + grid-template-columns: minmax(0, 1.08fr) minmax(320px, 0.92fr); + } + + .pane-grid.equal { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .compact-panel-body { + display: grid; + gap: 14px; + } + + .monitor-strip { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + } + + .monitor-card { + padding: 14px 16px; + border-radius: 16px; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.06); + } + + .monitor-card .title { + color: var(--muted); + font-size: 11px; + margin-bottom: 8px; + font-family: "IBM Plex Mono", monospace; + text-transform: uppercase; + letter-spacing: 0.08em; + } + + .monitor-card .main { + font-family: "IBM Plex Mono", monospace; + font-size: 16px; + color: var(--text); + margin-bottom: 6px; + } + + .monitor-card .meta { + color: var(--muted); + font-size: 12px; + line-height: 1.6; + } + .hero { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(360px, 0.9fr); @@ -2211,6 +2372,7 @@ } @media (max-width: 1240px) { + .console-hub, .hero, .layout, .priority-layout, @@ -2219,11 +2381,23 @@ grid-template-columns: 1fr; } + .console-sidebar { + position: static; + } + + .sidebar-nav { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .platform-grid, .signal-grid, .ops-grid, .coord-grid, - .position-card-grid { + .position-card-grid, + .pane-grid.two-col, + .pane-grid.equal, + .monitor-strip, + .runtime-summary-grid { grid-template-columns: 1fr; } @@ -2251,6 +2425,10 @@ padding-top: 12px; } + .sidebar-nav { + grid-template-columns: 1fr; + } + .hero-main, .hero-side, .panel, @@ -2294,7 +2472,7 @@
- 统一观察信号流、执行层、三端账户风险和平台熔断状态。 + 统一观察信号流、执行层、多账户风险和平台熔断状态。 这个页面的目标不是展示“历史”,而是让你在一屏内判断系统现在是不是健康、哪里堵住了、哪里需要人工接管。