Add hub settings tabs and account password change UI.

Restructure manual-trading-hub settings with CSS tabs matching instance layout; add password API writing HUB_USERNAME/HUB_PASSWORD to hub .env.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-08 22:01:26 +08:00
parent d339971c26
commit 88f7f0cfca
5 changed files with 494 additions and 202 deletions
+42
View File
@@ -1650,6 +1650,48 @@ def api_settings_meta():
}
class HubPasswordBody(BaseModel):
old_password: str = ""
new_username: str = ""
new_password: str = ""
confirm_password: str = ""
@app.post("/api/settings/password")
def api_change_hub_password(body: HubPasswordBody):
from hub_env_lib import update_hub_credentials
if not verify_credentials(expected_username(), body.old_password):
raise HTTPException(status_code=400, detail="当前密码错误")
if len(body.new_password or "") < 6:
raise HTTPException(status_code=400, detail="新密码至少 6 位")
if body.new_password != body.confirm_password:
raise HTTPException(status_code=400, detail="两次输入的新密码不一致")
new_user = (body.new_username or "").strip()
changed = update_hub_credentials(
new_password=body.new_password,
new_username=new_user or None,
)
if not changed:
return {"ok": True, "changed_keys": [], "restart_required": False}
return {"ok": True, "changed_keys": changed, "restart_required": True}
@app.get("/api/admin/health")
def api_admin_health():
return {"ok": True, "status": "up"}
@app.post("/api/admin/restart")
def api_admin_restart():
from hub_env_lib import restart_hub_pm2
result = restart_hub_pm2()
if not result.get("ok"):
raise HTTPException(status_code=500, detail=result.get("msg") or "restart failed")
return result
async def _fetch_agent_status(client: httpx.AsyncClient, ex: dict) -> dict:
url = f"{ex['agent_url'].rstrip('/')}/status"
try:
+53
View File
@@ -0,0 +1,53 @@
"""中控 .env 读写与 PM2 重启。"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
from typing import Any
from lib.env.env_file_lib import apply_env_updates, load_env_file_into_environ
HUB_DIR = Path(__file__).resolve().parent
def hub_env_path() -> str:
return str(HUB_DIR / ".env")
def update_hub_credentials(*, new_password: str, new_username: str | None = None) -> list[str]:
updates: dict[str, str] = {"HUB_PASSWORD": new_password}
if new_username:
updates["HUB_USERNAME"] = new_username
path = hub_env_path()
changed = apply_env_updates(path, updates)
if changed:
load_env_file_into_environ(path)
return changed
def restart_hub_pm2() -> dict[str, Any]:
app_name = (os.getenv("PM2_APP_NAME") or "").strip() or "manual-trading-hub"
if not sys.platform.startswith("linux"):
return {"ok": False, "msg": "仅 Linux 服务器支持 PM2 重启", "app": app_name}
try:
proc = subprocess.run(
["pm2", "restart", app_name, "--update-env"],
capture_output=True,
text=True,
timeout=120,
)
ok = proc.returncode == 0
return {
"ok": ok,
"app": app_name,
"msg": (proc.stdout or proc.stderr or "").strip()[:500],
"returncode": proc.returncode,
}
except FileNotFoundError:
return {"ok": False, "msg": "未找到 pm2 命令", "app": app_name}
except subprocess.TimeoutExpired:
return {"ok": False, "msg": "pm2 restart 超时", "app": app_name}
except Exception as e:
return {"ok": False, "msg": str(e), "app": app_name}
+119
View File
@@ -2989,6 +2989,125 @@ button.btn-sm {
margin-bottom: 0;
}
/* 中控系统设置 · CSS Tab(与实例 env 配置同方案) */
.hub-config-body {
margin-bottom: 16px;
}
.hub-tab-radio {
position: absolute;
opacity: 0;
pointer-events: none;
width: 0;
height: 0;
border: 0;
}
.hub-config-tabs {
display: flex;
flex-wrap: nowrap;
gap: 0;
overflow-x: auto;
border-bottom: 1px solid var(--border-soft);
padding: 0 8px;
scrollbar-width: thin;
}
.hub-tab-btn {
flex: 0 0 auto;
display: inline-block;
border: none;
background: transparent;
color: var(--muted);
font-size: 0.8rem;
padding: 10px 14px;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
white-space: nowrap;
transition: color 0.15s, border-color 0.15s;
user-select: none;
}
.hub-tab-btn:hover {
color: color-mix(in srgb, var(--text) 80%, var(--muted));
}
.hub-config-panels .hub-panel {
display: none;
}
#hub-sec-0:checked ~ .hub-config-tabs label[for="hub-sec-0"],
#hub-sec-1:checked ~ .hub-config-tabs label[for="hub-sec-1"],
#hub-sec-2:checked ~ .hub-config-tabs label[for="hub-sec-2"],
#hub-sec-3:checked ~ .hub-config-tabs label[for="hub-sec-3"],
#hub-sec-4:checked ~ .hub-config-tabs label[for="hub-sec-4"],
#hub-sec-5:checked ~ .hub-config-tabs label[for="hub-sec-5"] {
color: var(--text);
border-bottom-color: var(--accent);
font-weight: 600;
}
#hub-sec-0:checked ~ .hub-config-panels .hub-panel--0,
#hub-sec-1:checked ~ .hub-config-panels .hub-panel--1,
#hub-sec-2:checked ~ .hub-config-panels .hub-panel--2,
#hub-sec-3:checked ~ .hub-config-panels .hub-panel--3,
#hub-sec-4:checked ~ .hub-config-panels .hub-panel--4,
#hub-sec-5:checked ~ .hub-config-panels .hub-panel--5 {
display: block;
}
.hub-config-panels {
padding: 14px 16px 16px;
}
.hub-settings-tab-panel {
margin: 0;
}
.hub-settings-tab-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.hub-settings-tab-title {
margin: 0;
font-size: 1rem;
font-weight: 600;
}
.hub-settings-tab-head-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.hub-settings-tab-actions {
display: flex;
align-items: center;
gap: 10px;
margin-top: 12px;
flex-wrap: wrap;
}
.hub-password-grid {
margin-top: 8px;
}
.settings-status-line {
font-size: 0.8rem;
color: var(--muted);
}
.settings-status-line.is-err {
color: var(--danger, #f87171);
}
.settings-section {
margin-bottom: 16px;
}
+68 -16
View File
@@ -4168,6 +4168,8 @@
const parts = [];
if (m.password_required) parts.push("已启用用户名+密码登录");
else parts.push("未设 HUB_PASSWORD(反代公网暴露时建议设置 HUB_USERNAME + HUB_PASSWORD");
const userEl = document.getElementById("hub-pwd-current-user");
if (userEl && m.default_username) userEl.textContent = m.default_username;
if (m.hub_bridge_token_set) parts.push("中控已配置 HUB_BRIDGE_TOKEN");
else parts.push("中控未设 HUB_BRIDGE_TOKEN(实例需 APP_AUTH_DISABLED 或同令牌)");
if (m.public_origin) parts.push("浏览器外链基址: " + m.public_origin);
@@ -4258,21 +4260,6 @@
}
function initSettingsSectionFolds() {
document.querySelectorAll(".settings-section[data-settings-section]").forEach((el) => {
applySettingsSectionFold(el);
if (el.dataset.foldBound === "1") return;
el.dataset.foldBound = "1";
const foldBtn = el.querySelector(":scope > .settings-section-head > .settings-section-fold");
if (foldBtn) {
foldBtn.addEventListener("click", () => {
const section = el.dataset.settingsSection;
const collapsed = !el.classList.contains("is-collapsed");
el.classList.toggle("is-collapsed", collapsed);
foldBtn.setAttribute("aria-expanded", collapsed ? "false" : "true");
setSettingsFoldState(section, collapsed);
});
}
});
document.querySelectorAll(".settings-section-save").forEach((btn) => {
if (btn.dataset.saveBound === "1") return;
btn.dataset.saveBound = "1";
@@ -4290,12 +4277,76 @@
? "交易监管"
: section === "exchanges"
? "交易所账户"
: "设置";
: section === "backup"
? "备份设置"
: "设置";
if (section === "backup") return;
void saveSettingsSection(section, { label });
});
});
}
async function waitHubHealth() {
const deadline = Date.now() + 90000;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 2000));
try {
const r = await fetch("/api/admin/health", { credentials: "same-origin" });
if (r.ok) return;
} catch (_) {}
}
throw new Error("重启后中控未在预期时间内恢复");
}
async function saveHubPassword() {
const status = document.getElementById("hub-pwd-save-status");
const setStatus = (msg, err) => {
if (!status) return;
status.textContent = msg || "";
status.classList.toggle("is-err", !!err);
};
setStatus("保存中…");
try {
const r = await apiFetch("/api/settings/password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
old_password: (document.getElementById("hub-pwd-old") || {}).value || "",
new_username: (document.getElementById("hub-pwd-new-username") || {}).value || "",
new_password: (document.getElementById("hub-pwd-new") || {}).value || "",
confirm_password: (document.getElementById("hub-pwd-confirm") || {}).value || "",
}),
});
const j = await r.json();
if (!r.ok) throw new Error(j.detail || j.msg || "保存失败");
if (!j.restart_required) {
setStatus("未修改(与当前配置相同)");
showToast("未修改");
return;
}
setStatus("密码已保存,正在重启中控…");
await apiFetch("/api/admin/restart", { method: "POST" });
await waitHubHealth();
setStatus("密码已更新,请用新密码重新登录");
showToast("密码已更新,请重新登录");
setTimeout(() => {
location.href = "/login";
}, 1200);
} catch (e) {
setStatus(String(e.message || e), true);
showToast(String(e.message || e), true);
}
}
function initHubPasswordSettings() {
const btn = document.getElementById("hub-pwd-save-btn");
if (!btn || btn.dataset.bound === "1") return;
btn.dataset.bound = "1";
btn.addEventListener("click", () => {
void saveHubPassword();
});
}
function macroDatetimeLocalToApi(v) {
if (!v) return "";
return String(v).trim().replace("T", " ").slice(0, 16);
@@ -4439,6 +4490,7 @@
function loadSettingsUI() {
loadSettingsMetaLine();
initMacroCalendarSettings();
initHubPasswordSettings();
loadMacroCalendarUI();
loadSettings().then((data) => {
syncDisplayPrefsUI(data);
+212 -186
View File
@@ -15,7 +15,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260707-stats-bar-center" />
<link rel="stylesheet" href="/assets/app.css?v=20260708-hub-settings-tabs" />
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=3" />
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
<script src="/assets/account_risk_badge.js?v=4"></script>
@@ -885,208 +885,234 @@
<div id="page-settings" class="page hidden">
<div class="page-head">
<h1><span class="head-tag">CFG</span> 系统设置</h1>
<p class="page-desc">交易所地址、启用状态与监控能力</p>
<p class="page-desc">登录账号、导航显示、交易所地址与监控能力</p>
</div>
<details class="hint-box">
<summary>配置说明</summary>
<div class="hint-body">
保存后写入 <code>hub_settings.json</code>。Flask / Agent 填本机地址即可;复盘链接可留空(由 Flask 地址自动生成)<br />
<code>HUB_DISABLED_IDS</code> 可强制关闭账户;<code>HUB_BRIDGE_TOKEN</code> 与实例一致,或实例 <code>APP_AUTH_DISABLED=true</code><br />
公网反代请在 hub <code>.env</code> 设置 <code>HUB_USERNAME</code> <code>HUB_PASSWORD</code>(默认 <code>admin</code> / <code>admin123</code>);HTTPS 反代建议 <code>HUB_COOKIE_SECURE=true</code>
交易所等配置保存后写入 <code>hub_settings.json</code>;登录账号密码写入 <code>manual_trading_hub/.env</code><br />
Flask / Agent 填本机地址即可;复盘链接可留空(由 Flask 地址自动生成)<br />
<code>HUB_DISABLED_IDS</code> 可强制关闭账户;<code>HUB_BRIDGE_TOKEN</code> 与实例一致,或实例 <code>APP_AUTH_DISABLED=true</code>
</div>
</details>
<p id="settings-meta-line" class="settings-meta-line"></p>
<section class="settings-section card settings-display-panel" data-settings-section="display">
<div class="settings-section-head">
<button type="button" class="settings-section-fold" aria-expanded="true" aria-label="折叠"></button>
<h3 class="settings-display-title">显示与导航</h3>
<button type="button" class="primary settings-section-save" data-settings-section="display">保存</button>
<div class="hub-config-body card">
<input type="radio" name="hub-settings-section" id="hub-sec-0" class="hub-tab-radio" checked>
<input type="radio" name="hub-settings-section" id="hub-sec-1" class="hub-tab-radio">
<input type="radio" name="hub-settings-section" id="hub-sec-2" class="hub-tab-radio">
<input type="radio" name="hub-settings-section" id="hub-sec-3" class="hub-tab-radio">
<input type="radio" name="hub-settings-section" id="hub-sec-4" class="hub-tab-radio">
<input type="radio" name="hub-settings-section" id="hub-sec-5" class="hub-tab-radio">
<div class="hub-config-tabs" role="tablist" aria-label="系统设置分类">
<label for="hub-sec-0" class="hub-tab-btn" role="tab">账户密码</label>
<label for="hub-sec-1" class="hub-tab-btn" role="tab">显示与导航</label>
<label for="hub-sec-2" class="hub-tab-btn" role="tab">宏观数据</label>
<label for="hub-sec-3" class="hub-tab-btn" role="tab">交易监管</label>
<label for="hub-sec-4" class="hub-tab-btn" role="tab">备份恢复</label>
<label for="hub-sec-5" class="hub-tab-btn" role="tab">交易所账户</label>
</div>
<div class="settings-section-body">
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-account-pnl" checked />
监控区显示资金账户、交易账户与浮动盈亏
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-funds" checked />
顶栏显示「资金概况」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-dashboard" checked />
顶栏显示「数据看板」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-plan" checked />
顶栏显示「开仓计划」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-archive" checked />
顶栏显示「内照明心」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-ai" checked />
顶栏显示「AI 教练」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-calculator" checked />
顶栏显示「计算器」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-strategy" checked />
顶栏显示「策略说明」
</label>
<p class="settings-display-hint">保存至 hub_settings.json,换浏览器同样生效。关闭导航后对应页面将不可从顶栏进入,直接访问 URL 会跳回监控区。</p>
</div>
</section>
<section class="settings-section card settings-macro-panel" data-settings-section="macro">
<div class="settings-section-head">
<button type="button" class="settings-section-fold" aria-expanded="true" aria-label="折叠"></button>
<h3 class="settings-display-title">宏观关键数据(风控前置)</h3>
<button type="button" class="ghost settings-section-save" data-settings-section="macro">保存表单</button>
</div>
<div class="settings-section-body">
<p class="settings-display-hint">
手动录入 FOMC / CPI / 就业数据发布时间(北京时间)。监控区在发布前后各 1 小时提示风险:有仓注意仓位,无仓建议等待。仅提醒,不拦截下单。
</p>
<form id="macro-event-form" class="macro-event-form">
<label class="macro-event-field">
<span>数据名称</span>
<select id="macro-event-type" required>
<option value="fomc">FOMC 联邦基金利率</option>
<option value="cpi">美国 CPI 通胀</option>
<option value="employment">就业与劳工数据</option>
</select>
</label>
<label class="macro-event-field">
<span>发布时间(北京)</span>
<input id="macro-event-at" type="datetime-local" required />
</label>
<label class="macro-event-field macro-event-field-wide">
<span>备注(可选)</span>
<input id="macro-event-note" type="text" maxlength="500" placeholder="如:仅关注核心 CPI" autocomplete="off" />
</label>
<div class="macro-event-actions">
<button type="submit" id="macro-event-submit" class="primary">添加</button>
<button type="button" id="macro-event-cancel" class="ghost hidden">取消编辑</button>
</div>
</form>
<div id="macro-event-list" class="macro-event-list"></div>
</div>
</section>
<section class="settings-section card settings-supervisor-panel" data-settings-section="supervisor">
<div class="settings-section-head">
<button type="button" class="settings-section-fold" aria-expanded="true" aria-label="折叠"></button>
<h3 class="settings-display-title">交易监管 · 企业微信</h3>
<button type="button" class="primary settings-section-save" data-settings-section="supervisor">保存</button>
</div>
<div class="settings-section-body">
<p class="settings-display-hint">
与三所实例策略通知独立;手动/中控开平仓与新开仓会推送至此 Webhook。链接可在下方单独修改。
</p>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="supervisor-enabled" checked />
启用交易监管推送
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="supervisor-wechat-program" checked />
程序止盈/止损也发微信(鼓励向)
</label>
<div class="settings-grid supervisor-settings-grid">
<div class="field field-wide">
<label>企业微信 Webhook</label>
<input id="supervisor-wechat-webhook" type="text" placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." autocomplete="off" />
</div>
<div class="field field-wide">
<label>微信消息跳转链接(可改)</label>
<input id="supervisor-wechat-link" type="text" placeholder="https://你的域名/ai?mode=supervisor" autocomplete="off" />
</div>
<div class="field">
<label>消息前缀</label>
<input id="supervisor-wechat-prefix" type="text" value="【交易监管】" autocomplete="off" />
</div>
<div class="field">
<label>日手动平警告阈值</label>
<input id="supervisor-daily-warn" type="number" min="1" step="1" value="2" />
</div>
<div class="field">
<label>最短两笔间隔(分钟)</label>
<input id="supervisor-interval-warn" type="number" min="1" step="1" value="15" />
</div>
<div class="field">
<label>30 分钟内笔数阈值</label>
<input id="supervisor-freq-30m" type="number" min="1" step="1" value="2" />
</div>
<div class="field">
<label>平仓后再开仓(分钟)</label>
<input id="supervisor-reopen-min" type="number" min="1" step="1" value="30" />
</div>
</div>
</div>
</section>
<section class="settings-section card settings-backup-panel" data-settings-section="backup">
<div class="settings-section-head">
<button type="button" class="settings-section-fold" aria-expanded="true" aria-label="折叠"></button>
<h3 class="settings-display-title">备份与恢复</h3>
<button type="button" class="primary settings-section-save" data-settings-section="backup">保存</button>
</div>
<div class="settings-section-body">
<p class="settings-display-hint">
打包三所 <code>crypto.db</code>、中控 K 线/归档等 SQLite、<code>hub_settings.json</code><code>.env</code>(可选)。
恢复前会自动做一次 pre-restore 快照,并尝试 <code>pm2 restart all</code>
</p>
<div class="settings-grid backup-settings-grid">
<label class="chk-label settings-display-chk">
<input type="checkbox" id="backup-auto-enabled" checked />
每日自动备份(北京时间)
</label>
<div class="field">
<label>自动备份时刻(时,023</label>
<input id="backup-auto-hour" type="number" min="0" max="23" step="1" value="0" />
<div class="hub-config-panels">
<section class="hub-panel hub-panel--0 hub-settings-tab-panel" role="tabpanel">
<div class="hub-settings-tab-head">
<h3 class="hub-settings-tab-title">账户密码</h3>
</div>
<div class="field">
<label>保留天数</label>
<input id="backup-retention-days" type="number" min="1" max="365" step="1" value="30" />
<p class="settings-display-hint">修改中控网页登录账号密码,写入 <code>manual_trading_hub/.env</code> 后需重启中控生效。当前用户:<code id="hub-pwd-current-user">admin</code></p>
<div class="settings-grid hub-password-grid">
<div class="field"><label>当前密码</label><input type="password" id="hub-pwd-old" autocomplete="current-password"></div>
<div class="field"><label>新用户名(可选)</label><input type="text" id="hub-pwd-new-username" autocomplete="username"></div>
<div class="field"><label>新密码</label><input type="password" id="hub-pwd-new" autocomplete="new-password"></div>
<div class="field"><label>确认新密码</label><input type="password" id="hub-pwd-confirm" autocomplete="new-password"></div>
</div>
<div class="hub-settings-tab-actions">
<button type="button" class="primary" id="hub-pwd-save-btn">保存密码</button>
<span id="hub-pwd-save-status" class="settings-status-line"></span>
</div>
</section>
<section class="hub-panel hub-panel--1 hub-settings-tab-panel settings-display-panel" data-settings-section="display" role="tabpanel">
<div class="hub-settings-tab-head">
<h3 class="hub-settings-tab-title">显示与导航</h3>
<button type="button" class="primary settings-section-save" data-settings-section="display">保存</button>
</div>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="backup-include-env" checked />
包含 .env 配置文件
<input type="checkbox" id="pref-show-account-pnl" checked />
监控区显示资金账户、交易账户与浮动盈亏
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="backup-include-images" />
包含三所 static/images 截图
<input type="checkbox" id="pref-show-nav-funds" checked />
顶栏显示「资金概况」
</label>
<div class="field field-wide">
<label>备份目录(留空默认 /root/backups/crypto_monitor_portal</label>
<input id="backup-root" type="text" placeholder="/root/backups/crypto_monitor_portal" autocomplete="off" />
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-dashboard" checked />
顶栏显示「数据看板」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-plan" checked />
顶栏显示「开仓计划」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-archive" checked />
顶栏显示「内照明心」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-ai" checked />
顶栏显示「AI 教练」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-calculator" checked />
顶栏显示「计算器」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-strategy" checked />
顶栏显示「策略说明」
</label>
<p class="settings-display-hint">保存至 hub_settings.json,换浏览器同样生效。关闭导航后对应页面将不可从顶栏进入,直接访问 URL 会跳回监控区。</p>
</section>
<section class="hub-panel hub-panel--2 hub-settings-tab-panel settings-macro-panel" data-settings-section="macro" role="tabpanel">
<div class="hub-settings-tab-head">
<h3 class="hub-settings-tab-title">宏观关键数据(风控前置)</h3>
<button type="button" class="ghost settings-section-save" data-settings-section="macro">保存表单</button>
</div>
</div>
<div class="backup-actions">
<button type="button" id="backup-run-now" class="primary">立即备份</button>
<span id="backup-status-line" class="backup-status-line"></span>
</div>
<div class="backup-restore-upload">
<label class="backup-upload-label">
<span>上传备份包恢复(.zip</span>
<input id="backup-restore-file" type="file" accept=".zip,application/zip" />
<p class="settings-display-hint">
手动录入 FOMC / CPI / 就业数据发布时间(北京时间)。监控区在发布前后各 1 小时提示风险:有仓注意仓位,无仓建议等待。仅提醒,不拦截下单。
</p>
<form id="macro-event-form" class="macro-event-form">
<label class="macro-event-field">
<span>数据名称</span>
<select id="macro-event-type" required>
<option value="fomc">FOMC 联邦基金利率</option>
<option value="cpi">美国 CPI 通胀</option>
<option value="employment">就业与劳工数据</option>
</select>
</label>
<label class="macro-event-field">
<span>发布时间(北京)</span>
<input id="macro-event-at" type="datetime-local" required />
</label>
<label class="macro-event-field macro-event-field-wide">
<span>备注(可选)</span>
<input id="macro-event-note" type="text" maxlength="500" placeholder="如:仅关注核心 CPI" autocomplete="off" />
</label>
<div class="macro-event-actions">
<button type="submit" id="macro-event-submit" class="primary">添加</button>
<button type="button" id="macro-event-cancel" class="ghost hidden">取消编辑</button>
</div>
</form>
<div id="macro-event-list" class="macro-event-list"></div>
</section>
<section class="hub-panel hub-panel--3 hub-settings-tab-panel settings-supervisor-panel" data-settings-section="supervisor" role="tabpanel">
<div class="hub-settings-tab-head">
<h3 class="hub-settings-tab-title">交易监管 · 企业微信</h3>
<button type="button" class="primary settings-section-save" data-settings-section="supervisor">保存</button>
</div>
<p class="settings-display-hint">
与三所实例策略通知独立;手动/中控开平仓与新开仓会推送至此 Webhook。链接可在下方单独修改。
</p>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="supervisor-enabled" checked />
启用交易监管推送
</label>
<button type="button" id="backup-restore-upload-btn" class="danger">上传并恢复</button>
</div>
<div id="backup-list" class="backup-list"></div>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="supervisor-wechat-program" checked />
程序止盈/止损也发微信(鼓励向)
</label>
<div class="settings-grid supervisor-settings-grid">
<div class="field field-wide">
<label>企业微信 Webhook</label>
<input id="supervisor-wechat-webhook" type="text" placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." autocomplete="off" />
</div>
<div class="field field-wide">
<label>微信消息跳转链接(可改)</label>
<input id="supervisor-wechat-link" type="text" placeholder="https://你的域名/ai?mode=supervisor" autocomplete="off" />
</div>
<div class="field">
<label>消息前缀</label>
<input id="supervisor-wechat-prefix" type="text" value="【交易监管】" autocomplete="off" />
</div>
<div class="field">
<label>日手动平警告阈值</label>
<input id="supervisor-daily-warn" type="number" min="1" step="1" value="2" />
</div>
<div class="field">
<label>最短两笔间隔(分钟)</label>
<input id="supervisor-interval-warn" type="number" min="1" step="1" value="15" />
</div>
<div class="field">
<label>30 分钟内笔数阈值</label>
<input id="supervisor-freq-30m" type="number" min="1" step="1" value="2" />
</div>
<div class="field">
<label>平仓后再开仓(分钟)</label>
<input id="supervisor-reopen-min" type="number" min="1" step="1" value="30" />
</div>
</div>
</section>
<section class="hub-panel hub-panel--4 hub-settings-tab-panel settings-backup-panel" data-settings-section="backup" role="tabpanel">
<div class="hub-settings-tab-head">
<h3 class="hub-settings-tab-title">备份与恢复</h3>
<button type="button" class="primary settings-section-save" data-settings-section="backup">保存</button>
</div>
<p class="settings-display-hint">
打包三所 <code>crypto.db</code>、中控 K 线/归档等 SQLite、<code>hub_settings.json</code><code>.env</code>(可选)。
恢复前会自动做一次 pre-restore 快照,并尝试 <code>pm2 restart all</code>
</p>
<div class="settings-grid backup-settings-grid">
<label class="chk-label settings-display-chk">
<input type="checkbox" id="backup-auto-enabled" checked />
每日自动备份(北京时间)
</label>
<div class="field">
<label>自动备份时刻(时,023</label>
<input id="backup-auto-hour" type="number" min="0" max="23" step="1" value="0" />
</div>
<div class="field">
<label>保留天数</label>
<input id="backup-retention-days" type="number" min="1" max="365" step="1" value="30" />
</div>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="backup-include-env" checked />
包含 .env 配置文件
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="backup-include-images" />
包含三所 static/images 截图
</label>
<div class="field field-wide">
<label>备份目录(留空默认 /root/backups/crypto_monitor_portal</label>
<input id="backup-root" type="text" placeholder="/root/backups/crypto_monitor_portal" autocomplete="off" />
</div>
</div>
<div class="backup-actions">
<button type="button" id="backup-run-now" class="primary">立即备份</button>
<span id="backup-status-line" class="backup-status-line"></span>
</div>
<div class="backup-restore-upload">
<label class="backup-upload-label">
<span>上传备份包恢复(.zip</span>
<input id="backup-restore-file" type="file" accept=".zip,application/zip" />
</label>
<button type="button" id="backup-restore-upload-btn" class="danger">上传并恢复</button>
</div>
<div id="backup-list" class="backup-list"></div>
</section>
<section class="hub-panel hub-panel--5 hub-settings-tab-panel" data-settings-section="exchanges" role="tabpanel">
<div class="hub-settings-tab-head">
<h3 class="hub-settings-tab-title">交易所账户</h3>
<div class="hub-settings-tab-head-actions">
<button type="button" id="btn-settings-add" class="ghost">添加交易所</button>
<button type="button" class="primary settings-section-save" data-settings-section="exchanges">全部保存</button>
</div>
</div>
<div id="settings-list" class="settings-grid-wrap"></div>
</section>
</div>
</section>
<section class="settings-section card" data-settings-section="exchanges">
<div class="settings-section-head">
<button type="button" class="settings-section-fold" aria-expanded="true" aria-label="折叠"></button>
<h3 class="settings-display-title">交易所账户</h3>
<div class="settings-section-head-actions">
<button type="button" id="btn-settings-add" class="ghost">添加交易所</button>
<button type="button" class="primary settings-section-save" data-settings-section="exchanges">全部保存</button>
</div>
</div>
<div class="settings-section-body">
<div id="settings-list" class="settings-grid-wrap"></div>
</div>
</section>
</div>
<div class="toolbar settings-page-toolbar">
<button type="button" id="btn-settings-save" class="primary">保存全部</button>
<button type="button" id="btn-settings-reload" class="ghost">重新加载</button>