1
This commit is contained in:
parent
6ac175038e
commit
13f2845201
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
认证API
|
认证API
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, HTTPException, Depends, Request
|
from fastapi import APIRouter, HTTPException, Depends, Request, Response
|
||||||
from app.models.auth import (
|
from app.models.auth import (
|
||||||
SendCodeRequest, SendCodeResponse,
|
SendCodeRequest, SendCodeResponse,
|
||||||
LoginRequest, LoginResponse,
|
LoginRequest, LoginResponse,
|
||||||
@ -12,11 +12,54 @@ from app.services.auth_service import auth_service
|
|||||||
from app.services.jwt_service import jwt_service
|
from app.services.jwt_service import jwt_service
|
||||||
from app.services.db_service import db_service
|
from app.services.db_service import db_service
|
||||||
from app.middleware.auth_middleware import get_current_user, get_client_ip
|
from app.middleware.auth_middleware import get_current_user, get_client_ip
|
||||||
|
from app.middleware.console_auth import (
|
||||||
|
CONSOLE_AUTH_COOKIE,
|
||||||
|
create_console_access_token,
|
||||||
|
require_console_access,
|
||||||
|
)
|
||||||
|
from app.config import get_settings
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/auth", tags=["认证"])
|
router = APIRouter(prefix="/api/auth", tags=["认证"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/console-login")
|
||||||
|
async def console_login(request: Request, response: Response):
|
||||||
|
try:
|
||||||
|
body = await request.json()
|
||||||
|
except Exception:
|
||||||
|
body = {}
|
||||||
|
|
||||||
|
password = str(body.get("password") or "")
|
||||||
|
settings = get_settings()
|
||||||
|
if not password or password != settings.console_access_password:
|
||||||
|
raise HTTPException(status_code=403, detail="访问密码错误")
|
||||||
|
|
||||||
|
token = create_console_access_token()
|
||||||
|
max_age = max(1, int(settings.console_access_expire_days or 30)) * 24 * 60 * 60
|
||||||
|
response.set_cookie(
|
||||||
|
key=CONSOLE_AUTH_COOKIE,
|
||||||
|
value=token,
|
||||||
|
max_age=max_age,
|
||||||
|
httponly=True,
|
||||||
|
samesite="lax",
|
||||||
|
secure=False,
|
||||||
|
path="/",
|
||||||
|
)
|
||||||
|
return {"success": True, "message": "总控台登录成功"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/console-logout")
|
||||||
|
async def console_logout(response: Response):
|
||||||
|
response.delete_cookie(CONSOLE_AUTH_COOKIE, path="/")
|
||||||
|
return {"success": True, "message": "已退出总控台"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/console-session")
|
||||||
|
async def get_console_session(_: dict = Depends(require_console_access)):
|
||||||
|
return {"success": True, "authenticated": True}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/send-code", response_model=SendCodeResponse)
|
@router.post("/send-code", response_model=SendCodeResponse)
|
||||||
async def send_verification_code(
|
async def send_verification_code(
|
||||||
request_data: SendCodeRequest,
|
request_data: SendCodeRequest,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
交易 API
|
交易 API
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@ -13,6 +13,7 @@ from app.services.db_service import db_service
|
|||||||
from app.services.runtime_status_service import get_runtime_status
|
from app.services.runtime_status_service import get_runtime_status
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from app.crypto_agent.crypto_agent import get_crypto_agent
|
from app.crypto_agent.crypto_agent import get_crypto_agent
|
||||||
|
from app.middleware.console_auth import require_console_access
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/trading", tags=["交易"])
|
router = APIRouter(prefix="/api/trading", tags=["交易"])
|
||||||
@ -249,7 +250,7 @@ async def get_platform_halts():
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/execution-controls")
|
@router.get("/execution-controls")
|
||||||
async def get_execution_controls():
|
async def get_execution_controls(_: dict = Depends(require_console_access)):
|
||||||
"""获取目标级自动交易开关状态"""
|
"""获取目标级自动交易开关状态"""
|
||||||
try:
|
try:
|
||||||
agent = get_crypto_agent()
|
agent = get_crypto_agent()
|
||||||
@ -263,7 +264,7 @@ async def get_execution_controls():
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/execution-controls")
|
@router.post("/execution-controls")
|
||||||
async def set_execution_controls(request: ExecutionControlRequest):
|
async def set_execution_controls(request: ExecutionControlRequest, _: dict = Depends(require_console_access)):
|
||||||
"""设置目标级自动交易开关"""
|
"""设置目标级自动交易开关"""
|
||||||
try:
|
try:
|
||||||
agent = get_crypto_agent()
|
agent = get_crypto_agent()
|
||||||
@ -286,7 +287,7 @@ async def set_execution_controls(request: ExecutionControlRequest):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/platform-halts/resume")
|
@router.post("/platform-halts/resume")
|
||||||
async def resume_platform(request: ResumePlatformRequest):
|
async def resume_platform(request: ResumePlatformRequest, _: dict = Depends(require_console_access)):
|
||||||
"""手动恢复指定平台执行"""
|
"""手动恢复指定平台执行"""
|
||||||
try:
|
try:
|
||||||
agent = get_crypto_agent()
|
agent = get_crypto_agent()
|
||||||
|
|||||||
@ -2,10 +2,11 @@
|
|||||||
系统状态 API
|
系统状态 API
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
|
from app.middleware.console_auth import require_console_access
|
||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from app.utils.system_status import get_system_monitor
|
from app.utils.system_status import get_system_monitor
|
||||||
from app.crypto_agent.crypto_agent import get_crypto_agent
|
from app.crypto_agent.crypto_agent import get_crypto_agent
|
||||||
@ -339,7 +340,7 @@ async def get_agent_status(agent_id: str):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/console", response_model=Dict[str, Any])
|
@router.get("/console", response_model=Dict[str, Any])
|
||||||
async def get_console_snapshot():
|
async def get_console_snapshot(_: dict = Depends(require_console_access)):
|
||||||
"""
|
"""
|
||||||
获取总控台快照
|
获取总控台快照
|
||||||
|
|
||||||
|
|||||||
@ -64,6 +64,8 @@ class Settings(BaseSettings):
|
|||||||
# 安全配置
|
# 安全配置
|
||||||
secret_key: str = "change-this-secret-key-in-production"
|
secret_key: str = "change-this-secret-key-in-production"
|
||||||
rate_limit: str = "100/minute"
|
rate_limit: str = "100/minute"
|
||||||
|
console_access_password: str = "223388" # 总控台访问密码
|
||||||
|
console_access_expire_days: int = 30 # 总控台访问会话有效期
|
||||||
|
|
||||||
# JWT配置
|
# JWT配置
|
||||||
jwt_algorithm: str = "HS256"
|
jwt_algorithm: str = "HS256"
|
||||||
|
|||||||
@ -2447,20 +2447,24 @@ class CryptoAgent:
|
|||||||
if setup_type in {'trend_continuation_pullback', 'deep_pullback_continuation', 'breakout_pullback'}:
|
if setup_type in {'trend_continuation_pullback', 'deep_pullback_continuation', 'breakout_pullback'}:
|
||||||
if entry_type != 'limit':
|
if entry_type != 'limit':
|
||||||
return False, f"{setup_type} 应使用 limit 等待回踩/反抽"
|
return False, f"{setup_type} 应使用 limit 等待回踩/反抽"
|
||||||
|
if location_tag == 'far_from_trade_zone':
|
||||||
|
return False, f"{setup_type} 当前远离有效回踩交易区"
|
||||||
if pullback_quality != 'healthy_pullback':
|
if pullback_quality != 'healthy_pullback':
|
||||||
return False, f"{setup_type} 缺少健康缩量回调证据"
|
return True, f"{setup_type} 缺少健康缩量回调证据,降级执行"
|
||||||
if location_tag in {'far_from_trade_zone', 'middle_of_range'}:
|
if location_tag == 'middle_of_range':
|
||||||
return False, f"{setup_type} 当前不在有效回踩交易区"
|
return True, f"{setup_type} 当前位于区间中部,降级执行"
|
||||||
|
|
||||||
if setup_type == 'range_reversal':
|
if setup_type == 'range_reversal':
|
||||||
|
if location_tag == 'far_from_trade_zone':
|
||||||
|
return False, "区间反转 setup 远离关键交易区"
|
||||||
if location_tag not in {'near_range_support', 'near_range_resistance'}:
|
if location_tag not in {'near_range_support', 'near_range_resistance'}:
|
||||||
return False, "区间反转 setup 不在区间边界"
|
return True, "区间反转 setup 不在区间边界,降级执行"
|
||||||
if rejection_signal not in {'bullish_rejection', 'bearish_rejection'} and entry_type == 'market':
|
if rejection_signal not in {'bullish_rejection', 'bearish_rejection'} and entry_type == 'market':
|
||||||
return False, "区间反转现价执行缺少明确拒绝信号"
|
return False, "区间反转现价执行缺少明确拒绝信号"
|
||||||
|
|
||||||
if setup_type == 'trend_reversal':
|
if setup_type == 'trend_reversal':
|
||||||
if rejection_signal not in {'bullish_rejection', 'bearish_rejection'}:
|
if rejection_signal not in {'bullish_rejection', 'bearish_rejection'}:
|
||||||
return False, "趋势反转 setup 缺少拒绝/结构切换证据"
|
return True, "趋势反转 setup 缺少明确拒绝信号,降级执行"
|
||||||
|
|
||||||
if exhaustion_risk in {'upside_climax', 'downside_climax'} and setup_type != 'trend_reversal':
|
if exhaustion_risk in {'upside_climax', 'downside_climax'} and setup_type != 'trend_reversal':
|
||||||
return False, "当前量价处于高潮风险,非反转 setup 不执行"
|
return False, "当前量价处于高潮风险,非反转 setup 不执行"
|
||||||
@ -3995,6 +3999,9 @@ class CryptoAgent:
|
|||||||
setup_passed, setup_reason = self._check_setup_execution_constraints(signal)
|
setup_passed, setup_reason = self._check_setup_execution_constraints(signal)
|
||||||
if not setup_passed:
|
if not setup_passed:
|
||||||
return False, f"setup 过滤: {setup_reason}"
|
return False, f"setup 过滤: {setup_reason}"
|
||||||
|
if setup_reason != "setup 约束通过":
|
||||||
|
signal["_setup_execution_warning"] = setup_reason
|
||||||
|
logger.info(f"[{platform_name}] ⚠️ setup 降级执行: {setup_reason}")
|
||||||
|
|
||||||
current_leverage = account.get('current_total_leverage', 0)
|
current_leverage = account.get('current_total_leverage', 0)
|
||||||
max_leverage = account.get('max_total_leverage', 10)
|
max_leverage = account.get('max_total_leverage', 10)
|
||||||
|
|||||||
39
backend/app/middleware/console_auth.py
Normal file
39
backend/app/middleware/console_auth.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from fastapi import Cookie, HTTPException
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
|
||||||
|
from app.config import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
CONSOLE_AUTH_COOKIE = "console_access_token"
|
||||||
|
|
||||||
|
|
||||||
|
def create_console_access_token() -> str:
|
||||||
|
settings = get_settings()
|
||||||
|
expire = datetime.utcnow() + timedelta(days=max(1, int(settings.console_access_expire_days or 30)))
|
||||||
|
payload = {
|
||||||
|
"scope": "console_access",
|
||||||
|
"exp": expire,
|
||||||
|
"iat": datetime.utcnow(),
|
||||||
|
}
|
||||||
|
return jwt.encode(payload, settings.secret_key, algorithm=settings.jwt_algorithm)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_console_access_token(token: str) -> Dict:
|
||||||
|
settings = get_settings()
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.jwt_algorithm])
|
||||||
|
except JWTError as exc:
|
||||||
|
raise HTTPException(status_code=401, detail="总控台访问已失效,请重新登录") from exc
|
||||||
|
|
||||||
|
if payload.get("scope") != "console_access":
|
||||||
|
raise HTTPException(status_code=401, detail="总控台访问凭证无效")
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def require_console_access(console_access_token: str | None = Cookie(default=None, alias=CONSOLE_AUTH_COOKIE)) -> Dict:
|
||||||
|
if not console_access_token:
|
||||||
|
raise HTTPException(status_code=401, detail="请先登录总控台")
|
||||||
|
return verify_console_access_token(console_access_token)
|
||||||
@ -52,6 +52,70 @@
|
|||||||
mask-image: linear-gradient(180deg, rgba(0,0,0,0.45), rgba(0,0,0,0.9));
|
mask-image: linear-gradient(180deg, rgba(0,0,0,0.45), rgba(0,0,0,0.9));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: rgba(3, 8, 14, 0.82);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-overlay.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
width: min(420px, 100%);
|
||||||
|
padding: 28px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background: rgba(9, 18, 28, 0.96);
|
||||||
|
border: 1px solid rgba(128, 169, 202, 0.24);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card h2 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card p {
|
||||||
|
margin: 0 0 18px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-field {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 15px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-field:focus {
|
||||||
|
border-color: rgba(126, 200, 255, 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-error {
|
||||||
|
min-height: 20px;
|
||||||
|
margin-top: 12px;
|
||||||
|
color: var(--danger);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.console-shell {
|
.console-shell {
|
||||||
width: min(1600px, calc(100vw - 32px));
|
width: min(1600px, calc(100vw - 32px));
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@ -2466,6 +2530,19 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="auth-overlay" id="consoleAuthOverlay">
|
||||||
|
<div class="auth-card">
|
||||||
|
<div class="eyebrow">Console Access</div>
|
||||||
|
<h2>输入访问密码</h2>
|
||||||
|
<p>总控台包含账户权益、持仓、挂单和执行控制,需要先完成访问验证。</p>
|
||||||
|
<input id="consolePasswordInput" class="auth-field" type="password" placeholder="请输入总控台访问密码">
|
||||||
|
<div class="auth-actions">
|
||||||
|
<button class="action-btn action-primary" id="consoleLoginBtn">进入总控台</button>
|
||||||
|
</div>
|
||||||
|
<div class="auth-error" id="consoleAuthError"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="console-shell">
|
<div class="console-shell">
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="hero-card hero-main">
|
<div class="hero-card hero-main">
|
||||||
@ -2691,6 +2768,7 @@
|
|||||||
let cachedExecutionEvents = [];
|
let cachedExecutionEvents = [];
|
||||||
let cachedConsoleData = null;
|
let cachedConsoleData = null;
|
||||||
let revealSensitiveData = false;
|
let revealSensitiveData = false;
|
||||||
|
let consoleAuthenticated = false;
|
||||||
const SENSITIVE_VISIBILITY_KEY = 'console_sensitive_visible_v2';
|
const SENSITIVE_VISIBILITY_KEY = 'console_sensitive_visible_v2';
|
||||||
const HUB_PREFERENCE_KEY = 'console_hub_active_v1';
|
const HUB_PREFERENCE_KEY = 'console_hub_active_v1';
|
||||||
|
|
||||||
@ -2712,10 +2790,24 @@
|
|||||||
return Number.isFinite(num) ? `$${formatNumber(num, 2)}` : '-';
|
return Number.isFinite(num) ? `$${formatNumber(num, 2)}` : '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseConsoleDate(value) {
|
||||||
|
if (!value) return null;
|
||||||
|
if (value instanceof Date) return value;
|
||||||
|
const text = String(value).trim();
|
||||||
|
if (!text) return null;
|
||||||
|
|
||||||
|
const normalized = /([zZ]|[+-]\d{2}:\d{2})$/.test(text) ? text : `${text}Z`;
|
||||||
|
const date = new Date(normalized);
|
||||||
|
if (Number.isNaN(date.getTime())) return null;
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
function formatTime(value) {
|
function formatTime(value) {
|
||||||
if (!value) return '-';
|
if (!value) return '-';
|
||||||
try {
|
try {
|
||||||
return new Date(value).toLocaleString('zh-CN', { hour12: false });
|
const date = parseConsoleDate(value);
|
||||||
|
if (!date) return String(value);
|
||||||
|
return date.toLocaleString('zh-CN', { hour12: false, timeZone: 'Asia/Shanghai' });
|
||||||
} catch {
|
} catch {
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
@ -2723,7 +2815,8 @@
|
|||||||
|
|
||||||
function relativeTime(value) {
|
function relativeTime(value) {
|
||||||
if (!value) return '-';
|
if (!value) return '-';
|
||||||
const target = new Date(value);
|
const target = parseConsoleDate(value);
|
||||||
|
if (!target) return String(value);
|
||||||
if (Number.isNaN(target.getTime())) return String(value);
|
if (Number.isNaN(target.getTime())) return String(value);
|
||||||
const diff = Date.now() - target.getTime();
|
const diff = Date.now() - target.getTime();
|
||||||
const sec = Math.max(0, Math.floor(diff / 1000));
|
const sec = Math.max(0, Math.floor(diff / 1000));
|
||||||
@ -2772,6 +2865,64 @@
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showConsoleAuthOverlay(message = '') {
|
||||||
|
document.getElementById('consoleAuthOverlay').classList.remove('hidden');
|
||||||
|
document.getElementById('consoleAuthError').textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideConsoleAuthOverlay() {
|
||||||
|
document.getElementById('consoleAuthOverlay').classList.add('hidden');
|
||||||
|
document.getElementById('consoleAuthError').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureConsoleSession() {
|
||||||
|
const response = await fetch('/api/auth/console-session');
|
||||||
|
if (!response.ok) {
|
||||||
|
consoleAuthenticated = false;
|
||||||
|
showConsoleAuthOverlay(response.status === 401 ? '请输入访问密码后继续。' : '总控台认证失败');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
consoleAuthenticated = true;
|
||||||
|
hideConsoleAuthOverlay();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginConsole() {
|
||||||
|
const input = document.getElementById('consolePasswordInput');
|
||||||
|
const password = input.value.trim();
|
||||||
|
if (!password) {
|
||||||
|
showConsoleAuthOverlay('请输入访问密码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = document.getElementById('consoleLoginBtn');
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = '验证中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/console-login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ password })
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (!response.ok || !result.success) {
|
||||||
|
throw new Error(result.detail || result.message || '访问密码错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
input.value = '';
|
||||||
|
consoleAuthenticated = true;
|
||||||
|
hideConsoleAuthOverlay();
|
||||||
|
loadConsole();
|
||||||
|
} catch (error) {
|
||||||
|
showConsoleAuthOverlay(error.message || '登录失败');
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = '进入总控台';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSeverity(severity) {
|
function normalizeSeverity(severity) {
|
||||||
const value = String(severity || '').toLowerCase();
|
const value = String(severity || '').toLowerCase();
|
||||||
if (['danger', 'error', 'critical'].includes(value)) return 'danger';
|
if (['danger', 'error', 'critical'].includes(value)) return 'danger';
|
||||||
@ -2783,7 +2934,8 @@
|
|||||||
const cutoff = Date.now() - hours * 3600 * 1000;
|
const cutoff = Date.now() - hours * 3600 * 1000;
|
||||||
return (events || []).filter((event) => {
|
return (events || []).filter((event) => {
|
||||||
const rawTime = event?.timestamp || event?.created_at || event?.opened_at;
|
const rawTime = event?.timestamp || event?.created_at || event?.opened_at;
|
||||||
const time = rawTime ? new Date(rawTime).getTime() : 0;
|
const parsed = parseConsoleDate(rawTime);
|
||||||
|
const time = parsed ? parsed.getTime() : 0;
|
||||||
if (!time || Number.isNaN(time) || time < cutoff) return false;
|
if (!time || Number.isNaN(time) || time < cutoff) return false;
|
||||||
return typeof matcher === 'function' ? matcher(event) : true;
|
return typeof matcher === 'function' ? matcher(event) : true;
|
||||||
}).length;
|
}).length;
|
||||||
@ -2804,7 +2956,7 @@
|
|||||||
const protectionCoverage = positions.length > 0 ? (completeProtectionCount / positions.length) * 100 : 100;
|
const protectionCoverage = positions.length > 0 ? (completeProtectionCount / positions.length) * 100 : 100;
|
||||||
const conversionRate = recentSignal24h > 0 ? (success24h / recentSignal24h) * 100 : 0;
|
const conversionRate = recentSignal24h > 0 ? (success24h / recentSignal24h) * 100 : 0;
|
||||||
const staleEntryOrders = entryOrders.filter((order) => {
|
const staleEntryOrders = entryOrders.filter((order) => {
|
||||||
const created = order.created_at ? new Date(order.created_at).getTime() : 0;
|
const created = order.created_at ? (parseConsoleDate(order.created_at)?.getTime() || 0) : 0;
|
||||||
return created && !Number.isNaN(created) && (Date.now() - created) > (30 * 60 * 1000);
|
return created && !Number.isNaN(created) && (Date.now() - created) > (30 * 60 * 1000);
|
||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
@ -2937,17 +3089,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findMatchingEvent(signal, laneKeyword, events, platform) {
|
function findMatchingEvent(signal, laneKeyword, events, platform) {
|
||||||
const signalTime = signal?.created_at ? new Date(signal.created_at).getTime() : 0;
|
const signalTime = signal?.created_at ? (parseConsoleDate(signal.created_at)?.getTime() || 0) : 0;
|
||||||
const symbol = signal?.symbol;
|
const symbol = signal?.symbol;
|
||||||
const candidates = (events || []).filter((event) => {
|
const candidates = (events || []).filter((event) => {
|
||||||
if (event.symbol !== symbol) return false;
|
if (event.symbol !== symbol) return false;
|
||||||
if (!eventPlatformMatches(event, platform)) return false;
|
if (!eventPlatformMatches(event, platform)) return false;
|
||||||
if (laneKeyword && event.signal_timeframe_text && !String(event.signal_timeframe_text).includes(laneKeyword)) return false;
|
if (laneKeyword && event.signal_timeframe_text && !String(event.signal_timeframe_text).includes(laneKeyword)) return false;
|
||||||
const eventTime = event?.timestamp ? new Date(event.timestamp).getTime() : 0;
|
const eventTime = event?.timestamp ? (parseConsoleDate(event.timestamp)?.getTime() || 0) : 0;
|
||||||
if (!signalTime || !eventTime || Number.isNaN(signalTime) || Number.isNaN(eventTime)) return true;
|
if (!signalTime || !eventTime || Number.isNaN(signalTime) || Number.isNaN(eventTime)) return true;
|
||||||
return Math.abs(eventTime - signalTime) <= 12 * 3600 * 1000;
|
return Math.abs(eventTime - signalTime) <= 12 * 3600 * 1000;
|
||||||
});
|
});
|
||||||
return candidates.sort((a, b) => new Date(b.timestamp || 0) - new Date(a.timestamp || 0))[0] || null;
|
return candidates.sort((a, b) => {
|
||||||
|
const bt = parseConsoleDate(b.timestamp)?.getTime() || 0;
|
||||||
|
const at = parseConsoleDate(a.timestamp)?.getTime() || 0;
|
||||||
|
return bt - at;
|
||||||
|
})[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findMatchingPosition(signal, platform, positions) {
|
function findMatchingPosition(signal, platform, positions) {
|
||||||
@ -3948,6 +4104,10 @@
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!response.ok || !result.success) {
|
if (!response.ok || !result.success) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
consoleAuthenticated = false;
|
||||||
|
showConsoleAuthOverlay('访问会话已失效,请重新输入密码');
|
||||||
|
}
|
||||||
throw new Error(result.detail || result.message || '恢复失败');
|
throw new Error(result.detail || result.message || '恢复失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3978,6 +4138,10 @@
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!response.ok || !result.success) {
|
if (!response.ok || !result.success) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
consoleAuthenticated = false;
|
||||||
|
showConsoleAuthOverlay('访问会话已失效,请重新输入密码');
|
||||||
|
}
|
||||||
throw new Error(result.detail || result.message || '更新失败');
|
throw new Error(result.detail || result.message || '更新失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3992,8 +4156,17 @@
|
|||||||
|
|
||||||
async function loadConsole() {
|
async function loadConsole() {
|
||||||
try {
|
try {
|
||||||
|
if (!consoleAuthenticated) {
|
||||||
|
const ok = await ensureConsoleSession();
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
setFeedback('');
|
setFeedback('');
|
||||||
const response = await fetch('/api/system/console');
|
const response = await fetch('/api/system/console');
|
||||||
|
if (response.status === 401) {
|
||||||
|
consoleAuthenticated = false;
|
||||||
|
showConsoleAuthOverlay('访问会话已失效,请重新输入密码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.status !== 'success') {
|
if (result.status !== 'success') {
|
||||||
@ -4045,12 +4218,18 @@
|
|||||||
applyAutoRefreshState();
|
applyAutoRefreshState();
|
||||||
});
|
});
|
||||||
document.getElementById('toggleSensitiveBtn').addEventListener('click', toggleSensitiveData);
|
document.getElementById('toggleSensitiveBtn').addEventListener('click', toggleSensitiveData);
|
||||||
|
document.getElementById('consoleLoginBtn').addEventListener('click', loginConsole);
|
||||||
|
document.getElementById('consolePasswordInput').addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter') loginConsole();
|
||||||
|
});
|
||||||
|
|
||||||
loadSensitivePreference();
|
loadSensitivePreference();
|
||||||
initTabs();
|
initTabs();
|
||||||
initHubNavigation();
|
initHubNavigation();
|
||||||
applyAutoRefreshState();
|
applyAutoRefreshState();
|
||||||
loadConsole();
|
ensureConsoleSession().then((ok) => {
|
||||||
|
if (ok) loadConsole();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user