(function () {
  const toast = document.getElementById("toast");
  let settingsCache = null;
  let authState = { required: false, logged_in: true };

  function displayPref(key, defaultOn) {
    const d = settingsCache && settingsCache.display;
    if (!d || d[key] === undefined) return defaultOn !== false;
    return !!d[key];
  }

  function showAccountPnlPref() {
    return displayPref("show_account_pnl", true);
  }

  function showNavFundsPref() {
    return displayPref("show_nav_funds", true);
  }

  function showNavDashboardPref() {
    return displayPref("show_nav_dashboard", true);
  }

  function showNavAiPref() {
    return displayPref("show_nav_ai", true);
  }

  function showNavCalculatorPref() {
    return displayPref("show_nav_calculator", true);
  }

  function syncNavVisibility(data) {
    const d = (data && data.display) || {};
    const pairs = [
      ["nav-funds", "m-nav-funds", d.show_nav_funds === false],
      ["nav-dashboard", "m-nav-dashboard", d.show_nav_dashboard === false],
      ["nav-ai", "m-tab-ai", d.show_nav_ai === false],
      ["nav-calculator", "m-tab-calculator", d.show_nav_calculator === false],
    ];
    pairs.forEach(([desktopId, mobileId, hide]) => {
      const a = document.getElementById(desktopId);
      const b = document.getElementById(mobileId);
      if (a) a.classList.toggle("nav-hidden", hide);
      if (b) b.classList.toggle("nav-hidden", hide);
    });
  }

  const HUB_PHONE_PRIMARY = { monitor: 1, market: 1, calculator: 1, ai: 1 };

  /** 窄屏布局:仅按视口宽度,监控区/行情等共用;手机端 UI 不改 */
  function isMobileLayout() {
    return window.matchMedia("(max-width: 720px)").matches;
  }

  function syncHubPhoneShellClass() {
    document.body.classList.toggle("hub-phone", isMobileLayout());
    document.body.classList.remove("hub-tablet");
  }

  function closeHubMobileMore() {
    document.body.classList.remove("hub-mobile-more-open");
    const more = document.getElementById("hub-mobile-more");
    const btn = document.getElementById("m-tab-more");
    if (more) more.setAttribute("aria-hidden", "true");
    if (btn) btn.setAttribute("aria-expanded", "false");
    syncHubMobileTabActive(currentPage());
  }

  function openHubMobileMore() {
    if (!isMobileLayout()) return;
    document.body.classList.add("hub-mobile-more-open");
    const more = document.getElementById("hub-mobile-more");
    const btn = document.getElementById("m-tab-more");
    if (more) more.setAttribute("aria-hidden", "false");
    if (btn) btn.setAttribute("aria-expanded", "true");
    syncHubMobileTabActive(currentPage());
  }

  function toggleHubMobileMore() {
    if (document.body.classList.contains("hub-mobile-more-open")) closeHubMobileMore();
    else openHubMobileMore();
  }

  function syncHubMobileTabActive(page) {
    const primary = !!HUB_PHONE_PRIMARY[page];
    const moreOpen = document.body.classList.contains("hub-mobile-more-open");
    document.querySelectorAll(".hub-mobile-tabbar .hub-m-tab").forEach((el) => {
      const tab = el.getAttribute("data-hub-tab") || "";
      let on = false;
      if (tab === "more") on = moreOpen || !primary;
      else on = !moreOpen && tab === page;
      el.classList.toggle("active", on);
    });
    document.querySelectorAll(".hub-mobile-more-nav a").forEach((a) => {
      const href = (a.getAttribute("href") || "").split("?")[0];
      a.classList.toggle("active", href === "/" + page);
    });
  }

  function pageNavAllowed(page) {
    if (page === "funds") return showNavFundsPref();
    if (page === "dashboard") return showNavDashboardPref();
    if (
      page === "plan" ||
      page === "archive" ||
      page === "quotes" ||
      page === "strategy" ||
      page === "help" ||
      page === "logs"
    ) {
      return false;
    }
    if (page === "ai") return showNavAiPref();
    if (page === "calculator") return showNavCalculatorPref();
    return true;
  }

  function syncDisplayPrefsUI(data) {
    const d = (data && data.display) || {};
    const pnlCb = document.getElementById("pref-show-account-pnl");
    const fundsCb = document.getElementById("pref-show-nav-funds");
    const dashCb = document.getElementById("pref-show-nav-dashboard");
    const aiCb = document.getElementById("pref-show-nav-ai");
    const calcCb = document.getElementById("pref-show-nav-calculator");
    if (pnlCb) pnlCb.checked = d.show_account_pnl !== false;
    if (fundsCb) fundsCb.checked = d.show_nav_funds !== false;
    if (dashCb) dashCb.checked = d.show_nav_dashboard !== false;
    if (aiCb) aiCb.checked = d.show_nav_ai !== false;
    if (calcCb) calcCb.checked = d.show_nav_calculator !== false;
    syncNavVisibility(data);
  }

  function syncSupervisorSettingsUI(data) {
    const s = (data && data.supervisor) || {};
    const enabled = document.getElementById("supervisor-enabled");
    const prog = document.getElementById("supervisor-wechat-program");
    const webhook = document.getElementById("supervisor-wechat-webhook");
    const link = document.getElementById("supervisor-wechat-link");
    const prefix = document.getElementById("supervisor-wechat-prefix");
    const daily = document.getElementById("supervisor-daily-warn");
    const interval = document.getElementById("supervisor-interval-warn");
    const freq30 = document.getElementById("supervisor-freq-30m");
    const reopen = document.getElementById("supervisor-reopen-min");
    if (enabled) enabled.checked = s.enabled !== false;
    if (prog) prog.checked = s.wechat_on_program_tp_sl !== false;
    if (webhook) webhook.value = s.wechat_webhook || "";
    if (link) link.value = s.wechat_link_base || "";
    if (prefix) prefix.value = s.wechat_prefix || "【交易监管】";
    if (daily) daily.value = Number(s.manual_close_daily_warn) || 2;
    if (interval) interval.value = Number(s.interval_warn_minutes) || 15;
    if (freq30) freq30.value = Number(s.freq_30m_count) || 2;
    if (reopen) reopen.value = Number(s.reopen_after_close_minutes) || 30;
  }

  function positionTableHeadHtml(compact) {
    const pnlTh = showAccountPnlPref() ? "<th>浮盈</th>" : "";
    const cls = compact ? " data-table data-table-positions" : "";
    return `<table class="data-table${cls}"><thead><tr><th>合约</th><th>方向</th><th>开仓价</th><th>标记价</th><th>张数</th><th>盈利金额</th>${pnlTh}<th>操作</th></tr></thead><tbody>`;
  }
  let tpslPending = null;
  let lastMonitorRows = [];
  let monitorGridOptionsSplit = false;
  let lastMonitorTotals = null;
  let expandedExchangeId = sessionStorage.getItem("hub_expanded_ex") || "";
  const HUB_MONITOR_BOARD_CACHE_KEY = "hub_monitor_board_v1";
  const HUB_MONITOR_CACHE_MAX_AGE_MS = 6 * 60 * 60 * 1000;
  const MONITOR_BOARD_SNAPSHOT_URL = "/api/monitor/board/snapshot";
  const HUB_MONITOR_SNAPSHOT_TIMEOUT_MS = 15000;
  /** 关注:浮亏超过交易账户余额的比例(10%) */
  const HUB_ALERT_FLOAT_LOSS_RATIO = 0.1;
  let lastMonitorBoardUpdatedAt = "";
  let localBoardVersion = 0;
  let monitorBoardInFlight = false;
  let monitorBoardFetchPending = false;
  let monitorBoardSlowHintTimer = null;
  let boardEventSource = null;
  let sseReconnectTimer = null;
  let hostStatusTimer = null;
  const HOST_STATUS_POLL_MS = 5000;
  const HOST_STATUS_OPEN_KEY = "hub-host-status-open";
  const HOST_RESOURCE_ALERT_THRESHOLD = 85;
  const hostResourceAlertLatch = { cpu: false, mem: false };

  function loadBoolPref(key, defaultValue) {
    try {
      const raw = localStorage.getItem(key);
      if (raw === "1" || raw === "true") return true;
      if (raw === "0" || raw === "false") return false;
    } catch (_) {}
    return !!defaultValue;
  }

  function saveBoolPref(key, on) {
    try {
      localStorage.setItem(key, on ? "1" : "0");
    } catch (_) {}
  }

  function fmtHostBytes(n) {
    const v = Number(n);
    if (!Number.isFinite(v)) return "—";
    const abs = Math.abs(v);
    if (abs >= 1e12) return (v / 1e12).toFixed(2) + " TB";
    if (abs >= 1e9) return (v / 1e9).toFixed(2) + " GB";
    if (abs >= 1e6) return (v / 1e6).toFixed(2) + " MB";
    if (abs >= 1e3) return (v / 1e3).toFixed(1) + " KB";
    return v.toFixed(0) + " B";
  }

  function fmtHostUptime(sec) {
    const s = Math.max(0, Number(sec) || 0);
    const d = Math.floor(s / 86400);
    const h = Math.floor((s % 86400) / 3600);
    const m = Math.floor((s % 3600) / 60);
    if (d > 0) return d + "天" + h + "时";
    if (h > 0) return h + "时" + m + "分";
    return m + "分";
  }

  function hostMetricLevel(percent) {
    const p = Number(percent);
    if (!Number.isFinite(p)) return "ok";
    if (p >= HOST_RESOURCE_ALERT_THRESHOLD) return "bad";
    return "ok";
  }

  function hostOverallLevel(cpu, mem, disk) {
    const vals = [cpu && cpu.percent, mem && mem.percent, disk && disk.percent];
    for (let i = 0; i < vals.length; i++) {
      const p = Number(vals[i]);
      if (Number.isFinite(p) && p >= HOST_RESOURCE_ALERT_THRESHOLD) return "bad";
    }
    return "ok";
  }

  function setHostMetricBar(fillEl, percent) {
    if (!fillEl) return;
    const p = Math.max(0, Math.min(100, Number(percent) || 0));
    const level = hostMetricLevel(p);
    fillEl.style.width = p + "%";
    fillEl.classList.remove("warn", "bad", "ok");
    fillEl.classList.add(level === "bad" ? "bad" : "ok");
  }

  function checkHostResourceAlert(cpu, mem) {
    const msgs = [];
    const cpuP = Number(cpu && cpu.percent);
    if (Number.isFinite(cpuP) && cpuP >= HOST_RESOURCE_ALERT_THRESHOLD) {
      if (!hostResourceAlertLatch.cpu) {
        msgs.push("CPU 使用率 " + cpuP + "%");
        hostResourceAlertLatch.cpu = true;
      }
    } else {
      hostResourceAlertLatch.cpu = false;
    }
    const memP = Number(mem && mem.percent);
    if (Number.isFinite(memP) && memP >= HOST_RESOURCE_ALERT_THRESHOLD) {
      if (!hostResourceAlertLatch.mem) {
        msgs.push("内存使用率 " + memP + "%");
        hostResourceAlertLatch.mem = true;
      }
    } else {
      hostResourceAlertLatch.mem = false;
    }
    if (msgs.length) {
      window.alert(
        "服务器资源告警\n\n" + msgs.join("\n") + "\n\n请及时关注中控服务器负载."
      );
    }
  }

  function hostMetricSummaryHtml(label, percent) {
    const p = Number(percent);
    if (!Number.isFinite(p)) {
      return esc(label) + " —";
    }
    const tone = hostMetricLevel(p);
    return (
      esc(label) +
      ' <span class="host-metric-tone ' +
      tone +
      '">' +
      p +
      "%</span>"
    );
  }

  function renderHostStatusSummary(data, el) {
    if (!el) return;
    if (!data || !data.ok) {
      el.className = "host-status-summary-text bad";
      el.textContent = (data && data.msg) || "状态不可用";
      return;
    }
    const cpu = data.cpu || {};
    const mem = data.memory || {};
    const disk = data.disk || {};
    const parts = [];
    const host = String(data.hostname || "").trim();
    if (host) {
      parts.push('<span class="host-summary-host">' + esc(host) + "</span>");
    }
    if (cpu.percent != null) parts.push(hostMetricSummaryHtml("CPU", cpu.percent));
    if (mem.percent != null) parts.push(hostMetricSummaryHtml("内存", mem.percent));
    if (disk.percent != null) parts.push(hostMetricSummaryHtml("硬盘", disk.percent));
    el.className = "host-status-summary-text";
    el.innerHTML = parts.length
      ? parts.join(' <span class="host-summary-sep">·</span> ')
      : "—";
  }

  function setHostMetricVal(el, percent) {
    if (!el) return;
    const p = Number(percent);
    el.classList.remove("ok", "bad");
    if (!Number.isFinite(p)) {
      el.textContent = "—";
      return;
    }
    el.textContent = p + "%";
    el.classList.add(hostMetricLevel(p));
  }

  let hostStatusPanelInited = false;

  function initHostStatusPanel() {
    const panel = document.getElementById("host-status-panel");
    if (!panel) return;
    panel.classList.remove("hidden");
    if (!hostStatusPanelInited) {
      panel.open = loadBoolPref(HOST_STATUS_OPEN_KEY, false);
      panel.addEventListener("toggle", function () {
        saveBoolPref(HOST_STATUS_OPEN_KEY, !!panel.open);
      });
      hostStatusPanelInited = true;
    }
  }

  function renderHostStatusBar(data) {
    const panel = document.getElementById("host-status-panel");
    const summaryText = document.getElementById("host-status-summary-text");
    const bar = document.getElementById("host-status-bar");
    if (!panel || !bar) return;
    const dot = document.getElementById("host-status-dot");
    const name = document.getElementById("host-status-name");
    const uptime = document.getElementById("host-status-uptime");
    const updated = document.getElementById("host-status-updated");
    const cpuVal = document.getElementById("host-cpu-val");
    const cpuSub = document.getElementById("host-cpu-sub");
    const memVal = document.getElementById("host-mem-val");
    const memSub = document.getElementById("host-mem-sub");
    const diskVal = document.getElementById("host-disk-val");
    const diskSub = document.getElementById("host-disk-sub");
    const netUp = document.getElementById("host-net-up");
    const netDown = document.getElementById("host-net-down");
    panel.classList.remove("hidden");
    renderHostStatusSummary(data, summaryText);
    if (!data || !data.ok) {
      if (dot) dot.className = "host-status-dot bad";
      if (name) {
        name.textContent = "服务器";
        name.title = "";
      }
      if (uptime) uptime.textContent = (data && data.msg) || "状态不可用";
      if (updated) updated.textContent = "";
      if (cpuVal) cpuVal.textContent = "—";
      if (cpuSub) cpuSub.textContent = "";
      if (memVal) memVal.textContent = "—";
      if (memSub) memSub.textContent = "";
      if (diskVal) diskVal.textContent = "—";
      if (diskSub) diskSub.textContent = "";
      if (netUp) netUp.textContent = "↑ —";
      if (netDown) netDown.textContent = "↓ —";
      return;
    }
    const cpu = data.cpu || {};
    const mem = data.memory || {};
    const disk = data.disk || {};
    const net = data.network || {};
    checkHostResourceAlert(cpu, mem);
    const overall = hostOverallLevel(cpu, mem, disk);
    if (dot) dot.className = "host-status-dot " + overall;
    const hostname = data.hostname || "服务器";
    if (name) {
      name.textContent = hostname;
      name.title = hostname;
    }
    if (uptime) uptime.textContent = "运行 " + fmtHostUptime(data.uptime_sec);
    if (updated) updated.textContent = data.updated_at ? "更新 " + data.updated_at : "";
    setHostMetricBar(document.getElementById("host-cpu-fill"), cpu.percent);
    setHostMetricBar(document.getElementById("host-mem-fill"), mem.percent);
    setHostMetricBar(document.getElementById("host-disk-fill"), disk.percent);
    setHostMetricVal(cpuVal, cpu.percent);
    setHostMetricVal(memVal, mem.percent);
    setHostMetricVal(diskVal, disk.percent);
    if (cpuSub) cpuSub.textContent = cpu.count ? cpu.count + " 核" : "";
    if (memSub) {
      memSub.textContent =
        fmtHostBytes(mem.used_bytes) + " / " + fmtHostBytes(mem.total_bytes);
    }
    if (diskSub) {
      diskSub.textContent =
        fmtHostBytes(disk.used_bytes) + " / " + fmtHostBytes(disk.total_bytes);
    }
    if (netUp) netUp.textContent = "↑ " + fmtHostBytes(net.sent_rate_bps) + "/s";
    if (netDown) netDown.textContent = "↓ " + fmtHostBytes(net.recv_rate_bps) + "/s";
  }

  async function fetchHostStatus() {
    if (currentPage() !== "monitor") return;
    try {
      const r = await apiFetch("/api/host/status", { credentials: "same-origin" });
      const data = await r.json();
      renderHostStatusBar(data);
    } catch (err) {
      renderHostStatusBar({ ok: false, msg: String(err && err.message ? err.message : err) });
    }
  }

  function stopHostStatusPoll() {
    if (hostStatusTimer) {
      clearInterval(hostStatusTimer);
      hostStatusTimer = null;
    }
  }

  function startHostStatusPoll() {
    stopHostStatusPoll();
    initHostStatusPanel();
    void fetchHostStatus();
    hostStatusTimer = setInterval(fetchHostStatus, HOST_STATUS_POLL_MS);
  }

  async function apiFetch(url, opts) {
    const r = await fetch(url, opts);
    if (r.status === 401) {
      const next = encodeURIComponent(location.pathname + location.search);
      location.href = "/login?next=" + next;
      throw new Error("未登录");
    }
    return r;
  }

  let instanceFrameUrl = "";
  /** @type {{ exchangeId: string, nextPath: string, title: string } | null} */
  let instanceFrameCtx = null;

  function isHubEmbedded() {
    try {
      return window.self !== window.top;
    } catch (_) {
      return true;
    }
  }

  /** 在 LocalNav 等父页 iframe 内:直接替换本 iframe 地址,避免 postMessage / 三层嵌套 */
  function openInstanceInParentFrame(url) {
    try {
      window.location.assign(url);
      return true;
    } catch (_) {
      return false;
    }
  }

  async function fetchInstanceOpenUrl(exchangeId, nextPath, opts) {
    const options = opts || {};
    const next = nextPath || "/";
    const q = new URLSearchParams({ exchange_id: String(exchangeId), next });
    if (options.embed) q.set("embed", "1");
    if (options.embed && globalThis.HubTheme && typeof HubTheme.get === "function") {
      q.set("hub_theme", HubTheme.get());
    }
    const r = await apiFetch("/api/instance/open-url?" + q.toString());
    const j = await r.json();
    if (!j.ok || !j.url) {
      throw new Error(j.detail || "无法生成打开链接");
    }
    return j.url;
  }

  /** @type {number | null} */
  let instanceFrameNavLoadingTimer = null;

  function setInstanceFrameNavLoading(loading) {
    const shell = document.getElementById("instance-frame-shell");
    if (!shell) return;
    if (instanceFrameNavLoadingTimer != null) {
      clearTimeout(instanceFrameNavLoadingTimer);
      instanceFrameNavLoadingTimer = null;
    }
    if (loading) {
      instanceFrameNavLoadingTimer = window.setTimeout(() => {
        shell.classList.add("is-instance-nav-loading");
        instanceFrameNavLoadingTimer = null;
      }, 140);
      return;
    }
    shell.classList.remove("is-instance-nav-loading");
  }

  async function openInstance(exchangeId, nextPath, opts) {
    const options = opts || {};
    const newTab = !!options.newTab;
    const next = nextPath || "/";
    try {
      const embedded = isHubEmbedded();
      const url = await fetchInstanceOpenUrl(exchangeId, next, {
        embed: !newTab,
      });
      if (newTab) {
        window.open(url, "_blank", "noopener");
        return;
      }
      const row = lastMonitorRows.find((x) => String(x.id) === String(exchangeId));
      const title = row ? row.name : exchangeId;
      instanceFrameCtx = { exchangeId: String(exchangeId), nextPath: next, title };
      if (embedded) {
        try {
          window.parent.postMessage(
            {
              type: "hub:open-instance-nav",
              exchangeId: String(exchangeId),
              nextPath: next,
              title,
            },
            "*"
          );
        } catch (_) {}
        if (openInstanceInParentFrame(url)) return;
      }
      openInstanceFrame(url, title);
    } catch (e) {
      showToast(String(e), true);
    }
  }

  async function refreshInstanceFrame() {
    if (!instanceFrameCtx) {
      if (instanceFrameUrl) {
        const frame = document.getElementById("instance-frame");
        if (frame) frame.src = instanceFrameUrl;
      }
      return;
    }
    try {
      const url = await fetchInstanceOpenUrl(
        instanceFrameCtx.exchangeId,
        instanceFrameCtx.nextPath,
        { embed: true }
      );
      instanceFrameUrl = url;
      const frame = document.getElementById("instance-frame");
      if (frame) {
        setInstanceFrameNavLoading(true);
        frame.src = url;
      }
    } catch (e) {
      showToast(String(e), true);
    }
  }

  function openInstanceFrame(url, title) {
    const shell = document.getElementById("instance-frame-shell");
    const frame = document.getElementById("instance-frame");
    const titleEl = document.getElementById("instance-frame-title");
    if (!shell || !frame) {
      window.open(url, "_blank", "noopener");
      return;
    }
    closeExchangeFullscreen();
    instanceFrameUrl = url;
    if (titleEl) titleEl.textContent = title || "实例";
    setInstanceFrameNavLoading(true);
    frame.src = url;
    shell.classList.remove("hidden");
    shell.setAttribute("aria-hidden", "false");
    document.body.classList.add("hub-instance-frame-open");
    if (frame.dataset.themeSyncBound !== "1") {
      frame.dataset.themeSyncBound = "1";
      frame.addEventListener("load", function syncInstanceFrameTheme() {
        requestAnimationFrame(() => {
          try {
            if (globalThis.HubTheme && typeof HubTheme.get === "function" && frame.contentWindow) {
              frame.contentWindow.postMessage(
                { type: "hub-theme-sync", theme: HubTheme.get() },
                "*"
              );
            }
          } catch (_) {}
        });
      });
    }
  }

  function closeInstanceFrame() {
    const shell = document.getElementById("instance-frame-shell");
    const frame = document.getElementById("instance-frame");
    instanceFrameUrl = "";
    instanceFrameCtx = null;
    if (frame) frame.src = "about:blank";
    if (shell) {
      shell.classList.add("hidden");
      shell.setAttribute("aria-hidden", "true");
      shell.classList.remove("is-instance-nav-loading");
    }
    document.body.classList.remove("hub-instance-frame-open");
  }

  /** @deprecated use openInstance */
  async function openInstanceInBrowser(exchangeId, nextPath) {
    return openInstance(exchangeId, nextPath, { newTab: false });
  }

  async function initAuth() {
    try {
      const r = await fetch("/api/auth/status");
      authState = await r.json();
      const btn = document.getElementById("btn-logout");
      if (btn) btn.style.display = authState.required ? "" : "none";
      if (authState.required && !authState.logged_in) {
        location.href =
          "/login?next=" + encodeURIComponent(location.pathname + location.search);
        return false;
      }
      return true;
    } catch (_) {
      return true;
    }
  }

  function showToast(msg, isErr) {
    toast.textContent = msg;
    toast.style.borderColor = isErr ? "var(--red)" : "var(--border)";
    toast.classList.add("show");
    clearTimeout(showToast._t);
    showToast._t = setTimeout(() => toast.classList.remove("show"), 7000);
  }

  function esc(s) {
    return String(s)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  function formatRiskStatusBadge(riskStatus) {
    if (!riskStatus || typeof riskStatus !== "object") return "";
    if (window.AccountRiskBadge) return AccountRiskBadge.formatBadgeHtml(riskStatus, esc);
    const st = riskStatus.status || "normal";
    const label = esc(riskStatus.status_label || "正常");
    const title = esc(riskStatus.reason || "");
    return `<span class="risk-status-badge risk-status-${esc(st)}" role="status" title="${title}">${label}</span>`;
  }

  function fmt(n, d) {
    if (n === null || n === undefined || Number.isNaN(Number(n))) return "—";
    return Number(n).toLocaleString(undefined, { maximumFractionDigits: d });
  }

  /** 交易所持仓开仓价(三所子代理 entry_price) */
  function positionEntryPrice(pos) {
    if (!pos) return null;
    const n = Number(pos.entry_price);
    if (!Number.isFinite(n) || n <= 0) return null;
    return n;
  }

  function symbolPriceKey(sym) {
    return (sym || "").trim().toUpperCase();
  }

  function buildPriceTickMap(row) {
    const map = Object.create(null);
    const put = (sym, tick) => {
      const k = symbolPriceKey(sym);
      if (!k || tick == null || !Number.isFinite(Number(tick))) return;
      if (map[k] == null) map[k] = Number(tick);
    };
    ((row && row.agent && row.agent.positions) || []).forEach((p) => put(p.symbol, p.price_tick));
    const hm = (row && row.hub_monitor) || {};
    (hm.trends || []).forEach((t) => put(t.exchange_symbol || t.symbol, t.price_tick));
    (hm.orders || []).forEach((o) => put(o.exchange_symbol || o.symbol, o.price_tick));
    return map;
  }

  function lookupPriceTick(symbol, tickMap) {
    if (!tickMap || !symbol) return null;
    const k = symbolPriceKey(symbol);
    if (tickMap[k] != null) return tickMap[k];
    const base = normSym(symbol);
    if (base && tickMap[base] != null) return tickMap[base];
    return null;
  }

  function decimalsFromTick(tick) {
    if (tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0) return null;
    const t = Number(tick);
    if (t >= 1) return 0;
    const s = t.toFixed(12).replace(/0+$/, "");
    const frac = s.split(".")[1];
    return frac ? Math.min(12, frac.length) : 0;
  }

  function defaultPriceDecimals(value) {
    const n = Number(value);
    if (!Number.isFinite(n)) return 4;
    const av = Math.abs(n);
    if (av >= 10000) return 2;
    if (av >= 100) return 3;
    if (av >= 1) return 4;
    if (av >= 0.01) return 6;
    return 8;
  }

  /** 按交易所 tick(子代理/Flask 下发)格式化价格 */
  function fmtSymbolPrice(value, symbol, tickMap, displayFallback) {
    if (displayFallback != null && displayFallback !== "") return String(displayFallback);
    if (value == null || value === "") return "—";
    const n = Number(value);
    if (!Number.isFinite(n)) return "—";
    const tick = lookupPriceTick(symbol, tickMap);
    const d = decimalsFromTick(tick);
    return fmt(n, d != null ? d : defaultPriceDecimals(n));
  }

  function fmtEntryPrice(pos, tickMap) {
    if (pos && pos.entry_price_fmt) return String(pos.entry_price_fmt);
    return fmtSymbolPrice(positionEntryPrice(pos), pos && pos.symbol, tickMap);
  }

  function positionMarkPrice(pos) {
    if (!pos) return null;
    const n = Number(pos.mark_price);
    if (!Number.isFinite(n) || n <= 0) return null;
    return n;
  }

  function fmtMarkPrice(pos, tickMap) {
    if (pos && pos.mark_price_fmt) return String(pos.mark_price_fmt);
    return fmtSymbolPrice(positionMarkPrice(pos), pos && pos.symbol, tickMap);
  }

  function resolveTrendPositionRatioPct(trendPlan) {
    const t = trendPlan || {};
    if (t.position_ratio_pct != null && t.position_ratio_pct !== "") {
      const n = Number(t.position_ratio_pct);
      if (Number.isFinite(n)) return n;
    }
    const snap = Number(t.snapshot_available_usdt);
    const margin = Number(t.plan_margin_capital);
    if (Number.isFinite(snap) && snap > 0 && Number.isFinite(margin) && margin > 0) {
      return Math.round((margin / snap) * 10000) / 100;
    }
    return null;
  }

  function resolveTrendSizingFooter(mo, trendPlan, isTrend, pos) {
    const m = mo || {};
    const p = pos || {};
    if (!isTrend || !trendPlan || !trendPlan.id) {
      return {
        margin:
          m.exchange_initial_margin ??
          p.exchange_initial_margin ??
          m.plan_margin ??
          p.plan_margin ??
          null,
        leverage: m.leverage,
        planBase: m.margin_capital,
        positionRatio: m.position_ratio,
      };
    }
    const base =
      trendPlan.snapshot_available_usdt != null && trendPlan.snapshot_available_usdt !== ""
        ? trendPlan.snapshot_available_usdt
        : trendPlan.plan_margin_capital;
    return {
      margin: m.exchange_initial_margin ?? trendPlan.plan_margin_capital ?? null,
      leverage: trendPlan.leverage,
      planBase: base,
      positionRatio: resolveTrendPositionRatioPct(trendPlan),
    };
  }

  function resolvePositionOpenMeta(mo, trendPlan, isTrend) {
    const useTrend = isTrend && trendPlan && trendPlan.id;
    const src = useTrend ? trendPlan : mo || {};
    let ms = Number(src.opened_at_ms);
    if (!Number.isFinite(ms) || ms <= 0) {
      const s = String(src.opened_at || "").trim();
      if (s) {
        const parsed = Date.parse(s.replace(" ", "T"));
        ms = Number.isFinite(parsed) ? parsed : null;
      } else {
        ms = null;
      }
    } else {
      ms = Math.round(ms);
    }
    let display = "—";
    if (src.opened_at) {
      display = String(src.opened_at).replace("T", " ").slice(0, 16);
    } else if (ms) {
      display = new Date(ms).toISOString().slice(0, 16).replace("T", " ");
    }
    return { openedAtMs: ms, openedAtDisplay: display };
  }

  function formatLiveHoldDuration(openedMs, nowMs) {
    if (openedMs == null || !Number.isFinite(Number(openedMs))) return "—";
    const ms = Number(openedMs);
    const now = nowMs != null ? nowMs : Date.now();
    let sec = Math.floor((now - ms) / 1000);
    if (sec < 0) sec = 0;
    if (sec <= 0) return "0分钟";
    const d = Math.floor(sec / 86400);
    sec %= 86400;
    const h = Math.floor(sec / 3600);
    sec %= 3600;
    const m = Math.floor(sec / 60);
    const parts = [];
    if (d) parts.push(`${d}天`);
    if (h) parts.push(`${h}小时`);
    if (m || !parts.length) parts.push(`${m}分钟`);
    return parts.join("");
  }

  let hubHoldDurationTimer = null;

  function tickHubHoldDurations() {
    const now = Date.now();
    document.querySelectorAll(".pos-hold-duration[data-opened-ms]").forEach((el) => {
      const ms = Number(el.getAttribute("data-opened-ms"));
      if (!Number.isFinite(ms) || ms <= 0) return;
      el.textContent = formatLiveHoldDuration(ms, now);
    });
  }

  function ensureHubHoldDurationTimer() {
    tickHubHoldDurations();
    if (hubHoldDurationTimer) return;
    hubHoldDurationTimer = setInterval(tickHubHoldDurations, 1000);
  }

  function estimateLatestRiskUsdt(side, entry, sl, pos, mo) {
    const e = Number(entry);
    const s = Number(sl);
    if (!Number.isFinite(e) || !Number.isFinite(s) || e <= 0) return null;
    const sd = (side || "long").toLowerCase();
    const rf = sd === "short" ? (s - e) / e : (e - s) / e;
    if (!Number.isFinite(rf)) return null;
    if (rf <= 0) return 0;
    const m = mo || {};
    const p = pos || {};
    let notional = Number(p.notional_usdt);
    if (!Number.isFinite(notional) || notional <= 0) {
      notional = Number(m.exchange_notional);
    }
    if (!Number.isFinite(notional) || notional <= 0) {
      const mc = Number(m.margin_capital);
      const lev = Number(m.leverage);
      if (Number.isFinite(mc) && mc > 0 && Number.isFinite(lev) && lev > 0) {
        notional = mc * lev;
      }
    }
    if (!Number.isFinite(notional) || notional <= 0) {
      const c = Math.abs(Number(p.contracts));
      const cs = Number(p.contract_size);
      const mult = Number.isFinite(cs) && cs > 0 ? cs : 1;
      const px = Number(p.mark_price);
      const mark = Number.isFinite(px) && px > 0 ? px : e;
      if (Number.isFinite(c) && c > 0) notional = c * mult * mark;
    }
    if (!Number.isFinite(notional) || notional <= 0) return null;
    return Math.round(notional * rf * 100) / 100;
  }

  function formatLatestRiskMeta(mo, trendPlan, pos, tpsl) {
    const m = mo || {};
    const t = trendPlan || {};
    let v =
      m.latest_risk_amount != null && m.latest_risk_amount !== ""
        ? Number(m.latest_risk_amount)
        : pos && pos.latest_risk_amount != null && pos.latest_risk_amount !== ""
          ? Number(pos.latest_risk_amount)
          : t.latest_risk_amount != null && t.latest_risk_amount !== ""
            ? Number(t.latest_risk_amount)
            : null;
    if ((v == null || !Number.isFinite(v)) && tpsl && pos) {
      v = estimateLatestRiskUsdt(
        pos.side || m.direction,
        tpsl.entry,
        tpsl.sl,
        pos,
        m
      );
    }
    if (v != null && Number.isFinite(v)) {
      return `最新风险: ${fmt(v, 2)}U`;
    }
    return null;
  }

  function resolveTpProfitUsdt(mo, pos) {
    const m = mo || {};
    const p = pos || {};
    const raw =
      m.reward_at_tp_usdt != null && m.reward_at_tp_usdt !== ""
        ? m.reward_at_tp_usdt
        : p.reward_at_tp_usdt != null && p.reward_at_tp_usdt !== ""
          ? p.reward_at_tp_usdt
          : null;
    if (raw == null || raw === "") return null;
    const n = Number(raw);
    return Number.isFinite(n) ? n : null;
  }

  function formatTpProfitCell(mo, pos) {
    const n = resolveTpProfitUsdt(mo, pos);
    if (n == null) return "—";
    return `<span class="pos-tp-profit">${fmt(n, 2)}U</span>`;
  }

  function formatMonitorRiskMeta(mo, trendPlan) {
    const m = mo || {};
    const t = trendPlan || {};
    const amt =
      m.risk_amount != null && m.risk_amount !== ""
        ? Number(m.risk_amount)
        : t.risk_amount != null && t.risk_amount !== ""
          ? Number(t.risk_amount)
          : null;
    const pctRaw =
      m.risk_percent != null && m.risk_percent !== ""
        ? m.risk_percent
        : t.risk_percent != null && t.risk_percent !== ""
          ? t.risk_percent
          : null;
    if (pctRaw == null || pctRaw === "") {
      if (amt != null && Number.isFinite(amt)) {
        return `风险: ${fmt(amt, 2)}U`;
      }
      return null;
    }
    const pct = esc(pctRaw);
    if (amt != null && Number.isFinite(amt)) {
      return `风险: ${pct}%≈${fmt(amt, 2)}U`;
    }
    return `风险: ${pct}%`;
  }

  function resolveTrendMarkPrice(pos, trendPlan, symbol, tickMap) {
    const fromPos = fmtMarkPrice(pos, tickMap);
    if (fromPos && fromPos !== "—") return fromPos;
    const t = trendPlan || {};
    const sym = symbol || (pos && pos.symbol) || t.exchange_symbol || t.symbol || "";
    if (t.floating_mark != null && t.floating_mark !== "") {
      return fmtSymbolPrice(t.floating_mark, sym, tickMap);
    }
    if (t.last_mark_price != null && t.last_mark_price !== "") {
      return fmtSymbolPrice(t.last_mark_price, sym, tickMap);
    }
    return "—";
  }

  function estimateLinearSwapUpnl(side, entry, mark, contracts, contractSize) {
    const e = Number(entry);
    const m = Number(mark);
    const c = Math.abs(Number(contracts));
    let mult = Number(contractSize);
    if (!Number.isFinite(mult) || mult <= 0) mult = 1;
    if (!Number.isFinite(e) || !Number.isFinite(m) || !Number.isFinite(c) || c <= 0) {
      return null;
    }
    const diff =
      (side || "long").toLowerCase() === "long" ? m - e : e - m;
    return Math.round(diff * c * mult * 100) / 100;
  }

  /** 展示浮盈:子代理 unrealized_pnl;与 entry/mark/张数 推算偏差 >20% 时用推算值 */
  function resolvePositionUpnlUsdt(pos, trendPlan, markOverride) {
    const p = pos || {};
    const t = trendPlan || {};
    let exchange =
      p.unrealized_pnl != null && p.unrealized_pnl !== ""
        ? Number(p.unrealized_pnl)
        : null;
    if (exchange != null && !Number.isFinite(exchange)) exchange = null;
    const entry =
      t.avg_entry_price != null && t.avg_entry_price !== ""
        ? Number(t.avg_entry_price)
        : p.entry_price != null && p.entry_price !== ""
          ? Number(p.entry_price)
          : t.trigger_price != null
            ? Number(t.trigger_price)
            : null;
    let mark =
      markOverride != null && Number.isFinite(Number(markOverride))
        ? Number(markOverride)
        : p.mark_price != null && p.mark_price !== ""
          ? Number(p.mark_price)
          : t.floating_mark != null
            ? Number(t.floating_mark)
            : t.last_mark_price != null
              ? Number(t.last_mark_price)
              : null;
    const contracts = p.contracts;
    const cs =
      p.contract_size != null && p.contract_size !== ""
        ? Number(p.contract_size)
        : 1;
    const computed = estimateLinearSwapUpnl(
      p.side || t.direction,
      entry,
      mark,
      contracts,
      cs
    );
    if (computed == null) {
      if (exchange != null) return exchange;
      if (t.floating_pnl != null && t.floating_pnl !== "") {
        const n = Number(t.floating_pnl);
        if (Number.isFinite(n)) return n;
      }
      return null;
    }
    if (exchange == null) return computed;
    const ref = Math.max(Math.abs(computed), 1);
    if (Math.abs(exchange - computed) / ref > 0.2) return computed;
    return exchange;
  }

  function resolveTrendFloatingPnl(pos, trendPlan, markOverride) {
    return resolvePositionUpnlUsdt(pos, trendPlan, markOverride);
  }

  function formatFloatingPnlText(upnl, notionalUsdt) {
    if (upnl == null || !Number.isFinite(Number(upnl))) return { text: "—", cls: "" };
    let pnlText = fmt(upnl, 2) + "U";
    const notional = Number(notionalUsdt);
    if (Number.isFinite(notional) && Math.abs(notional) > 1e-8) {
      const pct = (Number(upnl) / Math.abs(notional)) * 100;
      pnlText += ` (${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%)`;
    }
    return { text: pnlText, cls: pnlCls(upnl) };
  }

  /** 与实例策略页一致:浮盈亏 % = 浮盈亏 / 计划保证金 */
  function formatTrendPlanFloatingPnl(upnl, planMargin) {
    if (upnl == null || !Number.isFinite(Number(upnl))) {
      return { text: "—", cls: "" };
    }
    let pnlText = fmt(upnl, 2) + "U";
    const margin = Number(planMargin);
    if (Number.isFinite(margin) && margin > 0) {
      const pct = (Number(upnl) / margin) * 100;
      pnlText += ` (${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%)`;
    }
    const n = Number(upnl);
    let cls = "pnl-neutral";
    if (n > 0) cls = "pnl-profit";
    else if (n < 0) cls = "pnl-loss";
    return { text: pnlText, cls };
  }

  function renderDirectionBadge(side) {
    const s = normSide(side);
    const label = sideDirLabel(side);
    const cls = s === "long" ? "direction-long" : s === "short" ? "direction-short" : "";
    if (!cls) return esc(String(label));
    return `<span class="badge ${cls}">${esc(label)}</span>`;
  }

  function resolveTrendDcaLevels(t) {
    if (Array.isArray(t.dca_levels) && t.dca_levels.length) return t.dca_levels;
    const plan = t || {};
    let grid = [];
    let legAmounts = [];
    try {
      grid = JSON.parse(plan.grid_prices_json || "[]");
      if (!Array.isArray(grid)) grid = [];
    } catch (_e) {
      grid = [];
    }
    try {
      legAmounts = JSON.parse(plan.leg_amounts_json || "[]");
      if (!Array.isArray(legAmounts)) legAmounts = [];
    } catch (_e2) {
      legAmounts = [];
    }
    const legsDone = Number(plan.legs_done) || 0;
    const dcaLegs = Number(plan.dca_legs) || 0;
    const firstDone = Number(plan.first_order_done) !== 0;
    const out = [
      {
        label: "首仓",
        price: null,
        contracts: plan.first_order_amount,
        status: firstDone ? "done" : "pending",
        status_label: firstDone ? "已开仓" : "待开仓",
      },
    ];
    const n = Math.max(grid.length, legAmounts.length, dcaLegs);
    for (let idx = 0; idx < n; idx += 1) {
      const legI = idx + 1;
      const done = legI <= legsDone;
      out.push({
        label: `补仓${legI}`,
        price: idx < grid.length ? grid[idx] : null,
        contracts: idx < legAmounts.length ? legAmounts[idx] : null,
        status: done ? "done" : "pending",
        status_label: done ? "已补仓" : "待补仓",
      });
    }
    return out;
  }

  function pnlCls(v) {
    const n = Number(v);
    if (!Number.isFinite(n) || n === 0) return "";
    return n > 0 ? "pnl-pos" : "pnl-neg";
  }

  function normSide(side) {
    const s = (side || "").toLowerCase();
    if (s === "buy") return "long";
    if (s === "sell") return "short";
    return s;
  }

  function sideDirCls(side) {
    const s = normSide(side);
    if (s === "long") return "side-long";
    if (s === "short") return "side-short";
    return "";
  }

  function sideDirLabel(side) {
    const s = normSide(side);
    if (s === "long") return "做多";
    if (s === "short") return "做空";
    return side || "—";
  }

  function isTrendHandoffOrder(monitorOrder) {
    const mo = monitorOrder || {};
    return String(mo.trade_style || "").toLowerCase() === "trend_pullback_handoff";
  }

  function isTrendContext(monitorOrder, trendPlan) {
    const mo = monitorOrder || {};
    const tp = trendPlan || {};
    if (tp.id != null && Number(tp.id) > 0) return true;
    const tid = Number(mo.trend_plan_id);
    if (Number.isFinite(tid) && tid > 0) return true;
    const mt = String(mo.monitor_type || "").trim();
    if (mt === "趋势回调") return true;
    const kst = String(mo.key_signal_type || "").trim();
    return kst === "趋势回调" || kst === "趋势回调计划";
  }

  function trendAddZoneLabel(direction) {
    return (direction || "long").toLowerCase() === "short" ? "补仓下沿" : "补仓上沿";
  }

  function monitorOrderSourceLabel(mo, trendPlan) {
    if (isTrendContext(mo, trendPlan)) return "趋势回调计划";
    const o = mo || {};
    const mt = String(o.monitor_type || "").trim();
    return mt || "下单监控";
  }

  function monitorEntryStyleHtml(mo, intradayDiscipline) {
    const o = mo || {};
    if (o.entry_model_label) return `开仓: ${esc(o.entry_model_label)}`;
    if (intradayDiscipline) return "开仓: —";
    const ts = String(o.trade_style || "").toLowerCase();
    if (ts === "swing") return "风格: 波段单";
    if (ts === "trend") return "风格: 趋势单";
    if (o.trade_style) return `风格: ${esc(o.trade_style)}`;
    return "风格: —";
  }

  function monitorOrderSourceHtml(mo, trendPlan) {
    if (isTrendContext(mo, trendPlan)) {
      return `来源: ${esc(monitorOrderSourceLabel(mo, trendPlan))}`;
    }
    const src = monitorOrderSourceLabel(mo, trendPlan);
    const kst = String((mo && mo.key_signal_type) || "").trim();
    let text = src;
    if (kst && kst !== src && !text.includes(kst)) {
      text += " · " + kst;
    }
    return `来源: ${esc(text)}`;
  }

  function renderDirectionHtml(side) {
    const cls = sideDirCls(side);
    const label = sideDirLabel(side);
    if (!cls) return esc(String(label));
    return `<span class="${cls}">${esc(label)}</span>`;
  }

  function keyHasPendingOrder(keyRow, keyPrice) {
    const kp = keyPrice || {};
    const oid = keyRow.fib_limit_order_id;
    if (oid != null && String(oid).trim() !== "") return true;
    const gm = String(kp.gate_metrics || "");
    if (gm.includes("限价单") || gm.includes("挂单")) return true;
    const gs = String(kp.gate_summary || "");
    if (/挂|限价|等待成交/.test(gs)) return true;
    return false;
  }

  function fmtKeyOrderAmount(keyRow) {
    const raw = keyRow.fib_order_amount;
    if (raw == null || raw === "") return "";
    const n = Number(raw);
    if (!Number.isFinite(n) || n <= 0) return "";
    return `${fmt(n, 4)} 张`;
  }

  /** 全屏持仓区:按仓位数量附加布局 class(1～6 固定列数,7+ 自动填充) */
  function hubPosListCountClass(n) {
    const c = Math.max(0, parseInt(n, 10) || 0);
    if (c <= 0) return "count-0";
    if (c <= 6) return `count-${c}`;
    return "count-many";
  }

  function currentPage() {
    const p = window.location.pathname.replace(/\/$/, "") || "/monitor";
    if (p.includes("settings")) return "settings";
    if (p.includes("archive")) return "archive";
    if (p.includes("quotes")) return "quotes";
    if (p.includes("dashboard")) return "dashboard";
    if (p.includes("funds")) return "funds";
    if (p.includes("plan")) return "plan";
    if (p.includes("calculator")) return "calculator";
    if (p.includes("help")) return "help";
    if (p.includes("strategy")) return "strategy";
    if (p.includes("logs")) return "logs";
    if (p.includes("market")) return "market";
    if (p.includes("/ai")) return "ai";
    return "monitor";
  }

  function pageElementId(page) {
    if (page === "settings") return "page-settings";
    if (page === "archive") return "page-archive";
    if (page === "quotes") return "page-quotes";
    if (page === "dashboard") return "page-dashboard";
    if (page === "funds") return "page-funds";
    if (page === "plan") return "page-plan";
    if (page === "calculator") return "page-calculator";
    if (page === "help") return "page-help";
    if (page === "strategy") return "page-strategy";
    if (page === "logs") return "page-logs";
    if (page === "market") return "page-market";
    if (page === "ai") return "page-ai";
    return "page-monitor";
  }

  function setActiveNav() {
    let page = currentPage();
    if (!pageNavAllowed(page)) {
      history.replaceState({}, "", "/monitor");
      page = "monitor";
    }
    const pageId = pageElementId(page);
    document.querySelectorAll(".top-nav a").forEach((a) => {
      const href = (a.getAttribute("href") || "").split("?")[0];
      a.classList.toggle(
        "active",
        href === "/" + page || (page === "monitor" && (href === "/" || href === "/monitor"))
      );
    });
    document.querySelectorAll(".page").forEach((el) => {
      el.classList.toggle("hidden", el.id !== pageId);
    });
    document.body.classList.toggle("hub-page-ai", page === "ai");
    document.body.classList.toggle("hub-page-funds", page === "funds");
    document.body.classList.toggle("hub-page-dashboard", page === "dashboard");
    document.body.classList.toggle("hub-page-monitor", page === "monitor");
    document.body.classList.toggle("hub-page-market", page === "market");
    document.body.classList.toggle("hub-page-calculator", page === "calculator");
    document.body.classList.toggle("hub-page-settings", page === "settings");
    document.body.classList.toggle("hub-page-archive", page === "archive");
    document.body.classList.toggle("hub-page-quotes", page === "quotes");
    document.body.classList.toggle("hub-page-plan", page === "plan");
    document.body.classList.toggle("hub-page-strategy", page === "strategy");
    document.body.classList.toggle("hub-page-logs", page === "logs");
    document.body.classList.toggle("hub-page-help", page === "help");
    syncHubPhoneShellClass();
    if (HUB_PHONE_PRIMARY[page]) closeHubMobileMore();
    syncHubMobileTabActive(page);
    syncHubAiMobileViewport();
    if (page === "monitor") startMonitorPoll();
    else stopMonitorPoll();
    if (page !== "ai") closeSupervisorStream();
    if (page === "dashboard" && window.hubDashboardPage) {
      window.hubDashboardPage.init();
    } else if (window.hubDashboardPage && window.hubDashboardPage.destroy) {
      window.hubDashboardPage.destroy();
    }
    if (page === "settings") loadSettingsUI();
    if (page === "ai") loadAiPage();
    if (page === "archive" && window.hubArchivePage) {
      window.hubArchivePage.init();
    } else if (window.hubArchivePage && window.hubArchivePage.destroy) {
      window.hubArchivePage.destroy();
    }
    if (page === "quotes" && window.hubQuotesPage) {
      window.hubQuotesPage.init();
    } else if (window.hubQuotesPage && window.hubQuotesPage.destroy) {
      window.hubQuotesPage.destroy();
    }
    if (page === "plan" && window.hubPlanPage) {
      window.hubPlanPage.init();
    } else if (window.hubPlanPage && window.hubPlanPage.destroy) {
      window.hubPlanPage.destroy();
    }
    if (page === "calculator" && window.hubCalculatorPage) {
      window.hubCalculatorPage.init();
    }
    if (page === "funds" && window.hubFundsPage) {
      window.hubFundsPage.init();
    } else if (window.hubFundsPage && window.hubFundsPage.destroy) {
      window.hubFundsPage.destroy();
    }
    if (page === "strategy" && window.hubStrategyPage) {
      window.hubStrategyPage.init();
    } else if (window.hubStrategyPage && window.hubStrategyPage.destroy) {
      window.hubStrategyPage.destroy();
    }
    if (page === "help" && window.hubHelpPage) {
      window.hubHelpPage.init();
    } else if (window.hubHelpPage && window.hubHelpPage.destroy) {
      window.hubHelpPage.destroy();
    }
    if (page === "logs" && window.hubLogsPage) {
      window.hubLogsPage.init();
    } else if (window.hubLogsPage && window.hubLogsPage.destroy) {
      window.hubLogsPage.destroy();
    }
    if (page === "market" && window.hubMarketChart) {
      window.hubMarketChart.init();
    } else if (window.hubMarketChart) {
      if (window.hubMarketChart.stopChartLive) window.hubMarketChart.stopChartLive();
      else {
        if (window.hubMarketChart.stopAutoRefresh) window.hubMarketChart.stopAutoRefresh();
      }
      if (window.hubMarketChart.stopPriceTagTimer) window.hubMarketChart.stopPriceTagTimer();
    }
  }

  function stopMonitorPoll() {
    closeMonitorBoardStream();
    stopHostStatusPoll();
    stopMacroBannerPoll();
    if (sseReconnectTimer) {
      clearTimeout(sseReconnectTimer);
      sseReconnectTimer = null;
    }
  }

  function closeMonitorBoardStream() {
    if (boardEventSource) {
      boardEventSource.close();
      boardEventSource = null;
    }
  }

  function connectMonitorBoardStream() {
    closeMonitorBoardStream();
    if (!document.getElementById("auto-monitor")?.checked) return;
    if (currentPage() !== "monitor") return;
    boardEventSource = new EventSource("/api/monitor/board/stream");
    boardEventSource.addEventListener("board", (ev) => {
      try {
        const st = JSON.parse(ev.data || "{}");
        const ver = Number(st.board_version) || 0;
        if (ver !== localBoardVersion) {
          void fetchMonitorBoardSnapshot({ background: true });
        } else if (st.aggregating && lastMonitorRows.length) {
          applyMonitorBoardUi(lastMonitorRows, st.updated_at || lastMonitorBoardUpdatedAt, {
            stale: true,
          });
        }
      } catch (_) {}
    });
    boardEventSource.onerror = () => {
      closeMonitorBoardStream();
      if (sseReconnectTimer) clearTimeout(sseReconnectTimer);
      sseReconnectTimer = setTimeout(() => {
        if (currentPage() === "monitor" && document.getElementById("auto-monitor")?.checked) {
          connectMonitorBoardStream();
          void fetchMonitorBoardSnapshot({ background: true });
        }
      }, 8000);
    };
  }

  async function requestMonitorBoardRefresh() {
    await apiFetch("/api/monitor/board/refresh", { method: "POST" });
  }

  function clearMonitorBoardSlowHint() {
    if (monitorBoardSlowHintTimer) {
      clearTimeout(monitorBoardSlowHintTimer);
      monitorBoardSlowHintTimer = null;
    }
  }

  function scheduleMonitorBoardSlowHint(box) {
    clearMonitorBoardSlowHint();
    if (!box) return;
    monitorBoardSlowHintTimer = setTimeout(() => {
      if (lastMonitorRows.length) return;
      const el = box.querySelector(".board-loading");
      if (!el) return;
      const sub = el.querySelector(".board-loading-sub");
      if (sub) {
        sub.textContent =
          "后台首次聚合较慢(三所子代理 + Flask).可检查 PM2,或设 HUB_BOARD_KEY_PRICES=false 加速.";
      }
    }, 12000);
  }

  function saveMonitorBoardCache(rows, updatedAt, boardVersion, totals) {
    try {
      sessionStorage.setItem(
        HUB_MONITOR_BOARD_CACHE_KEY,
        JSON.stringify({
          version: 2,
          board_version: boardVersion != null ? boardVersion : localBoardVersion,
          updated_at: updatedAt || "",
          rows: rows || [],
          totals: totals || null,
          saved_at: Date.now(),
        })
      );
    } catch (_) {}
  }

  function loadMonitorBoardFromCache() {
    try {
      const raw = sessionStorage.getItem(HUB_MONITOR_BOARD_CACHE_KEY);
      if (!raw) return null;
      const data = JSON.parse(raw);
      if (!data || !Array.isArray(data.rows) || !data.rows.length) return null;
      const age = Date.now() - Number(data.saved_at || 0);
      if (!Number.isFinite(age) || age > HUB_MONITOR_CACHE_MAX_AGE_MS) {
        sessionStorage.removeItem(HUB_MONITOR_BOARD_CACHE_KEY);
        return null;
      }
      return data;
    } catch (_) {
      return null;
    }
  }

  function restoreMonitorBoardFromCache() {
    const cached = loadMonitorBoardFromCache();
    if (!cached) return false;
    lastMonitorRows = cached.rows;
    lastMonitorTotals = cached.totals || null;
    lastMonitorBoardUpdatedAt = cached.updated_at || "";
    localBoardVersion = 0;
    applyMonitorBoardUi(cached.rows, lastMonitorBoardUpdatedAt, { stale: true });
    return true;
  }

  function applyMonitorBoardUi(rows, updatedAt, opts) {
    const options = opts || {};
    const tsRaw = updatedAt || lastMonitorBoardUpdatedAt || "";
    if (updatedAt) lastMonitorBoardUpdatedAt = updatedAt;
    const online = (rows || []).filter((x) => x.http_ok && (x.agent || {}).ok !== false).length;
    const pill = document.getElementById("sys-status");
    if (pill) {
      pill.textContent = rows.length ? `LINK ${online}/${rows.length}` : "NO DATA";
      pill.classList.toggle("warn", rows.length && online < rows.length);
      if (options.stale) pill.classList.add("syncing");
      else pill.classList.remove("syncing");
    }
    const upd = document.getElementById("monitor-updated");
    const updSum = document.getElementById("monitor-updated-summary");
    if (upd || updSum) {
      const ts = tsRaw.replace("T", " ");
      const txt = options.stale
        ? ts
          ? `缓存 ${ts} · 后台聚合中…`
          : "后台聚合中…"
        : ts
          ? `UPD ${ts}`
          : "";
      if (upd) upd.textContent = txt;
      if (updSum) updSum.textContent = txt;
    }
    updateMonitorAlertSummary(rows || []);
    void refreshMacroRiskBanner(rows || []);
    renderMonitorGrid(rows || []);
  }

  let macroBannerTimer = null;
  let macroCalendarEditId = null;

  function monitorHasOpenPositions(rows) {
    return (rows || []).some((row) => {
      const pos = (row.agent && row.agent.positions) || [];
      return Array.isArray(pos) && pos.length > 0;
    });
  }

  function macroAlertMessage(alert, hasPositions) {
    const label = alert.event_type_label || alert.event_type || "宏观数据";
    const phase = alert.phase || "window";
    const mins = Number(alert.minutes_to_event || 0);
    if (hasPositions) {
      if (phase === "imminent" && mins > 0) {
        return (
          `「${label}」即将发布(约 ${mins} 分钟),` +
          "注意仓位风险:勿加仓,检查止损/减仓"
        );
      }
      return `「${label}」高波动窗口(±1h),注意仓位风险:勿加仓,检查止损/减仓`;
    }
    if (phase === "imminent" && mins > 0) {
      return `「${label}」即将发布(约 ${mins} 分钟),建议等待,避免新开仓`;
    }
    return `「${label}」高波动窗口(±1h),建议等待,避免新开仓`;
  }

  async function refreshMacroRiskBanner(rows) {
    if (currentPage() !== "monitor") return;
    const el = document.getElementById("monitor-macro-banner");
    const textEl = document.getElementById("monitor-macro-banner-text");
    if (!el || !textEl) return;
    try {
      const r = await apiFetch("/api/macro-calendar/active");
      const j = await r.json();
      const alerts = (j.ok && j.alerts) || [];
      if (!alerts.length) {
        el.classList.add("hidden");
        el.classList.remove("phase-imminent");
        textEl.textContent = "";
        return;
      }
      const alert = alerts[0];
      const hasPos = monitorHasOpenPositions(rows || lastMonitorRows);
      textEl.textContent = macroAlertMessage(alert, hasPos);
      el.classList.toggle("phase-imminent", alert.phase === "imminent");
      el.classList.remove("hidden");
    } catch (_) {
      el.classList.add("hidden");
    }
  }

  function startMacroBannerPoll() {
    stopMacroBannerPoll();
    if (currentPage() !== "monitor") return;
    void refreshMacroRiskBanner(lastMonitorRows);
    macroBannerTimer = setInterval(() => {
      if (currentPage() === "monitor") void refreshMacroRiskBanner(lastMonitorRows);
    }, 30000);
  }

  function stopMacroBannerPoll() {
    if (macroBannerTimer) {
      clearInterval(macroBannerTimer);
      macroBannerTimer = null;
    }
  }

  function startMonitorPoll() {
    const hadCache = restoreMonitorBoardFromCache();
    void fetchMonitorBoardSnapshot({ showLoading: !hadCache });
    connectMonitorBoardStream();
    startHostStatusPoll();
    startMacroBannerPoll();
  }

  async function loadSettings() {
    const r = await apiFetch("/api/settings");
    settingsCache = await r.json();
    syncNavVisibility(settingsCache);
    return settingsCache;
  }

  function enabledAccounts() {
    return (settingsCache?.exchanges || []).filter((x) => x.enabled);
  }

  /** AI 教练手机布局:窄屏或手机 PWA(桌面安装的 App 仍走桌面布局) */
  function isMobileAiLayout() {
    if (isMobileLayout()) return true;
    if (
      window.matchMedia("(display-mode: standalone)").matches &&
      window.matchMedia("(max-width: 960px)").matches
    ) {
      return true;
    }
    if (window.navigator && window.navigator.standalone === true) return true;
    return false;
  }

  function positionHasContracts(p) {
    const c = Number(p && p.contracts);
    return Number.isFinite(c) && Math.abs(c) >= 1e-12;
  }

  function exchangeNeedsFlask(row) {
    const caps = row.capabilities || [];
    return caps.includes("key") || caps.includes("trend");
  }

  function positionMissingStopLoss(pos, orders, trends) {
    if (!positionHasContracts(pos)) return false;
    const mo = findMonitorOrder(orders, pos.symbol, pos.side);
    const tp = findTrendPlan(trends, pos.symbol, pos.side);
    const tpsl = resolvePositionTpsl(pos, mo, tp);
    const sl = tpsl.sl;
    if (sl !== "" && sl != null && Number.isFinite(Number(sl))) return false;
    const cond = condOrdersFromPosition(pos);
    const picked = pickExTpslOrders(cond);
    if (picked.sl && picked.sl.trigger_price != null) return false;
    const et = pos.exchange_tpsl;
    if (et && et.sl) return false;
    return true;
  }

  function analyzeExchangeAlert(row) {
    const ag = row.agent || {};
    const hm = row.hub_monitor || {};
    const pos = Array.isArray(ag.positions) ? ag.positions : [];
    const flaskOk = row.flask_ok !== false && hm.ok !== false;
    const upnl = Number(ag.total_unrealized_pnl);
    const tradingBal = Number(row.trading_usdt);
    const balance =
      Number.isFinite(tradingBal) && tradingBal > 0
        ? tradingBal
        : Number(ag.balance_usdt);
    const sortUpnl = Number.isFinite(upnl) ? upnl : 0;

    if (!row.http_ok) {
      return { level: "error", summary: "子代理离线", sortUpnl: 0 };
    }
    if (ag.ok === false) {
      return {
        level: "error",
        summary: (ag.error || row.error || "子代理异常").slice(0, 24),
        sortUpnl: 0,
      };
    }
    if (exchangeNeedsFlask(row) && !flaskOk) {
      const fe = row.flask_error || hm.error || hm.msg || "Flask未连通";
      return { level: "error", summary: String(fe).slice(0, 24), sortUpnl };
    }

    const orders = flaskOk ? hm.orders || [] : [];
    const trends = flaskOk ? hm.trends || [] : [];
    let missingSl = false;
    for (const p of pos) {
      if (positionMissingStopLoss(p, orders, trends)) {
        missingSl = true;
        break;
      }
    }

    if (Number.isFinite(upnl) && upnl < 0 && Number.isFinite(balance) && balance > 0) {
      const lossPct = (Math.abs(upnl) / balance) * 100;
      if (lossPct >= HUB_ALERT_FLOAT_LOSS_RATIO * 100) {
        return {
          level: "warn",
          summary: `浮亏超10% · ${fmt(upnl, 2)}U`,
          sortUpnl,
        };
      }
    }
    if (missingSl) {
      return { level: "warn", summary: "缺止损", sortUpnl };
    }

    const openCount = pos.filter(positionHasContracts).length;
    return {
      level: "ok",
      summary: openCount ? "正常" : "空仓",
      sortUpnl,
    };
  }

  function sortRowsForMobileDashboard(rows) {
    const levelOrder = { error: 0, warn: 1, ok: 2 };
    return rows
      .map((r) => ({ r, a: analyzeExchangeAlert(r) }))
      .sort((x, y) => {
        const ld = levelOrder[x.a.level] - levelOrder[y.a.level];
        if (ld !== 0) return ld;
        return (x.a.sortUpnl || 0) - (y.a.sortUpnl || 0);
      })
      .map((x) => x.r);
  }

  function updateMonitorAlertSummary(rows) {
    const el = document.getElementById("monitor-alert-summary");
    if (!el) return;
    if (!isMobileLayout() || !rows.length) {
      el.classList.add("hidden");
      el.innerHTML = "";
      return;
    }
    let err = 0;
    let warn = 0;
    let ok = 0;
    rows.forEach((r) => {
      const lv = analyzeExchangeAlert(r).level;
      if (lv === "error") err += 1;
      else if (lv === "warn") warn += 1;
      else ok += 1;
    });
    el.classList.remove("hidden");
    el.innerHTML = `<span class="mas-item mas-ok">正常 ${ok}</span><span class="mas-sep">·</span><span class="mas-item mas-warn">关注 ${warn}</span><span class="mas-sep">·</span><span class="mas-item mas-err">异常 ${err}</span>`;
  }

  /** 监控卡片列数:桌面 2×2(统计+三所);期权分栏时由 CSS 控制 */
  function syncMonitorGridColumns(gridEl, itemCount, opts) {
    if (!gridEl) return;
    const options = opts || {};
    if (options.optionsSplit) {
      gridEl.style.gridTemplateColumns = "";
      return;
    }
    if (isMobileLayout()) {
      // 手机一律单列:统计卡与三所卡不再并排挤字
      gridEl.style.gridTemplateColumns = "1fr";
      return;
    }
    if (options.statsFirst) {
      gridEl.style.gridTemplateColumns = "repeat(2, minmax(0, 1fr))";
      return;
    }
    let cols = 3;
    if (itemCount <= 1) cols = 1;
    else if (itemCount === 2) cols = 2;
    else if (itemCount === 3) cols = 3;
    else if (itemCount === 4) cols = 2;
    else cols = 3;
    gridEl.style.gridTemplateColumns = `repeat(${cols}, minmax(0, 1fr))`;
  }

  const AI_MOBILE_TAB_KEY = "hub_ai_mobile_tab";
  const AI_MOBILE_CHAT_TABS = new Set(["trading", "general", "supervisor"]);
  let aiSupervisorSessionCache = null;
  let supervisorEventSource = null;
  let localSupervisorVersion = 0;
  let supervisorReconnectTimer = null;

  function isSupervisorMode() {
    return aiSelectedBotMode === "supervisor";
  }

  function normalizeAiBotMode(mode) {
    const m = (mode || "").trim().toLowerCase();
    if (m === "general") return "general";
    if (m === "supervisor") return "supervisor";
    return "trading";
  }

  function normalizeAiMobileTab(tab) {
    const raw = (tab || "").trim().toLowerCase();
    if (raw === "chat") return "trading";
    if (AI_MOBILE_CHAT_TABS.has(raw) || raw === "history") return raw;
    return "trading";
  }

  function applyAiMobileTab(tab) {
    const layout = document.querySelector(".ai-layout");
    const tabs = document.querySelectorAll(".ai-mobile-tab");
    if (!layout) return;
    const mobile = isMobileAiLayout();
    if (!mobile) {
      delete layout.dataset.aiMobileTab;
      tabs.forEach((btn) => {
        btn.classList.remove("is-active");
        btn.setAttribute("aria-selected", "false");
      });
      return;
    }
    const active = normalizeAiMobileTab(
      tab || localStorage.getItem(AI_MOBILE_TAB_KEY) || "trading"
    );
    layout.dataset.aiMobileTab = active;
    tabs.forEach((btn) => {
      const t = btn.dataset.aiTab || "";
      const on = t === active;
      btn.classList.toggle("is-active", on);
      btn.setAttribute("aria-selected", on ? "true" : "false");
    });
    if (AI_MOBILE_CHAT_TABS.has(active)) {
      updateAiBotTabs(active);
      if (active === "supervisor") {
        void loadAiSupervisorSession().then(() => connectSupervisorStream());
      } else {
        closeSupervisorStream();
      }
      scrollAiChatToEnd();
    }
    if (active === "history") {
      const hist = document.getElementById("ai-chat-history-list");
      if (hist) hist.scrollTop = 0;
    }
  }

  function initAiMobileTabs() {
    const tabs = document.querySelectorAll(".ai-mobile-tab");
    if (!tabs.length) return;
    tabs.forEach((btn) => {
      btn.addEventListener("click", () => {
        const tab = btn.dataset.aiTab || "trading";
        if (tab === "new") {
          const prev = normalizeAiMobileTab(localStorage.getItem(AI_MOBILE_TAB_KEY) || "trading");
          const botMode = prev === "general" ? "general" : prev === "supervisor" ? "supervisor" : "trading";
          if (botMode === "supervisor") {
            void switchToSupervisorMode();
          } else {
            void newAiChat(botMode);
          }
          return;
        }
        if (tab === "supervisor") {
          void switchToSupervisorMode();
          return;
        }
        localStorage.setItem(AI_MOBILE_TAB_KEY, tab);
        applyAiMobileTab(tab);
        if (AI_MOBILE_CHAT_TABS.has(tab)) {
          const input = document.getElementById("ai-chat-input");
          if (input && isMobileAiLayout()) input.focus();
        }
      });
    });
    window.addEventListener("resize", () => applyAiMobileTab());
    applyAiMobileTab();
  }

  let syncHubAiMobileViewport = () => {};

  function initHubAiMobileViewport() {
    const shell = document.querySelector(".app-shell");
    const chatInput = document.getElementById("ai-chat-input");
    if (!shell || !window.visualViewport) {
      syncHubAiMobileViewport = () => {};
      return;
    }

    let baselineInnerH = Math.max(window.innerHeight, window.visualViewport.height || 0);

    const scrollChatToEnd = () => {
      const box = document.getElementById("ai-chat-messages");
      if (box) requestAnimationFrame(() => { box.scrollTop = box.scrollHeight; });
    };

    syncHubAiMobileViewport = () => {
      const onAi = document.body.classList.contains("hub-page-ai");
      if (!onAi || !isMobileAiLayout()) {
        shell.style.removeProperty("height");
        shell.style.removeProperty("max-height");
        shell.style.removeProperty("width");
        shell.style.removeProperty("transform");
        document.documentElement.style.removeProperty("--hub-vvh");
        document.body.classList.remove("hub-ai-keyboard-open");
        return;
      }
      const vv = window.visualViewport;
      const h = Math.max(240, Math.round(vv.height));
      const top = Math.round(vv.offsetTop || 0);
      const left = Math.round(vv.offsetLeft || 0);
      const inputFocused = !!(chatInput && document.activeElement === chatInput);
      if (!inputFocused) {
        baselineInnerH = Math.max(baselineInnerH, window.innerHeight, h);
      }
      document.documentElement.style.setProperty("--hub-vvh", `${h}px`);
      shell.style.height = `${h}px`;
      shell.style.maxHeight = `${h}px`;
      shell.style.width = `${Math.round(vv.width)}px`;
      shell.style.transform =
        top > 0 || left > 0 ? `translate(${left}px, ${top}px)` : "";
      const viewportShrunk = h < baselineInnerH * 0.72;
      const keyboardLikely = inputFocused && (viewportShrunk || top > 48);
      document.body.classList.toggle("hub-ai-keyboard-open", keyboardLikely);
    };

    window.visualViewport.addEventListener("resize", syncHubAiMobileViewport);
    window.visualViewport.addEventListener("scroll", syncHubAiMobileViewport);
    window.addEventListener("resize", syncHubAiMobileViewport);
    window.addEventListener("orientationchange", () => {
      setTimeout(syncHubAiMobileViewport, 80);
    });

    if (chatInput) {
      chatInput.addEventListener("focus", () => {
        syncHubAiMobileViewport();
        scrollChatToEnd();
        setTimeout(syncHubAiMobileViewport, 50);
        setTimeout(syncHubAiMobileViewport, 280);
      });
      chatInput.addEventListener("blur", () => {
        setTimeout(syncHubAiMobileViewport, 80);
        setTimeout(syncHubAiMobileViewport, 320);
      });
    }
    syncHubAiMobileViewport();
  }

  function syncHubPhoneFolds() {
    const phone = isMobileLayout();
    ["monitor-ops-fold", "market-toolbar-fold"].forEach((id) => {
      const el = document.getElementById(id);
      if (!el) return;
      if (!phone) {
        el.open = true;
        return;
      }
      // 手机默认折叠;用户手动点开后本会话不再强关
      if (el.dataset.userToggled === "1") return;
      el.open = false;
    });
  }

  function bindHubPhoneFolds() {
    ["monitor-ops-fold", "market-toolbar-fold"].forEach((id) => {
      const el = document.getElementById(id);
      if (!el || el.dataset.boundFold === "1") return;
      el.dataset.boundFold = "1";
      el.addEventListener("toggle", () => {
        if (!isMobileLayout()) return;
        el.dataset.userToggled = "1";
      });
    });
    const sym = document.getElementById("market-symbol");
    const tf = document.getElementById("market-timeframe");
    const syncMarketFoldMeta = () => {
      const meta = document.getElementById("market-fold-summary");
      if (!meta) return;
      const s = sym ? String(sym.value || "").trim() : "";
      const t = tf ? String(tf.value || "").trim() : "";
      meta.textContent = [s || "—", t || "—"].join(" · ");
    };
    if (sym) sym.addEventListener("change", syncMarketFoldMeta);
    if (sym) sym.addEventListener("input", syncMarketFoldMeta);
    if (tf) tf.addEventListener("change", syncMarketFoldMeta);
    syncMarketFoldMeta();
  }

  function initHubMobileChrome() {
    const moreBtn = document.getElementById("m-tab-more");
    const backdrop = document.getElementById("hub-mobile-more-backdrop");
    const closeBtn = document.getElementById("hub-mobile-more-close");
    if (moreBtn) {
      moreBtn.addEventListener("click", (ev) => {
        ev.preventDefault();
        toggleHubMobileMore();
      });
    }
    if (backdrop) backdrop.addEventListener("click", closeHubMobileMore);
    if (closeBtn) closeBtn.addEventListener("click", closeHubMobileMore);
    document.addEventListener("keydown", (ev) => {
      if (ev.key === "Escape" && document.body.classList.contains("hub-mobile-more-open")) {
        closeHubMobileMore();
      }
    });
    bindHubPhoneFolds();
    syncHubPhoneShellClass();
    syncHubPhoneFolds();
  }

  function bindHubSpaNavLinks(selector) {
    document.querySelectorAll(selector).forEach((a) => {
      a.addEventListener("click", (ev) => {
        const href = a.getAttribute("href");
        if (!href || ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return;
        ev.preventDefault();
        closeHubMobileMore();
        const path = href.split("?")[0];
        if (path === window.location.pathname) {
          setActiveNav();
          return;
        }
        history.pushState({}, "", href);
        setActiveNav();
      });
    });
  }

  function initMobileLayout() {
    initAiMobileTabs();
    initHubAiMobileViewport();
    initHubMobileChrome();
    let resizeTimer = null;
    let wasMobile = isMobileLayout();
    window.addEventListener("resize", () => {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(() => {
        const nowMobile = isMobileLayout();
        syncHubPhoneShellClass();
        syncHubPhoneFolds();
        if (!nowMobile) closeHubMobileMore();
        if (lastMonitorRows.length && nowMobile !== wasMobile) {
          wasMobile = nowMobile;
          renderMonitorGrid(lastMonitorRows);
          updateMonitorAlertSummary(lastMonitorRows);
          syncHubMobileTabActive(currentPage());
          return;
        }
        wasMobile = nowMobile;
        const box = document.getElementById("monitor-grid");
        if (box && lastMonitorRows.length) {
          const split = monitorOptionsSplitActive(lastMonitorRows);
          syncMonitorGridColumns(box, lastMonitorRows.length + (lastMonitorTotals ? 1 : 0), {
            statsFirst: !!lastMonitorTotals && !split,
            optionsSplit: split,
          });
          updateMonitorAlertSummary(lastMonitorRows);
        }
        syncHubMobileTabActive(currentPage());
      }, 120);
    });
  }

  function normSym(s) {
    return String(s || "")
      .toUpperCase()
      .replace(/:USDT$/i, "")
      .replace(/\/USDT:USDT$/i, "")
      .replace(/\/USDT$/i, "");
  }

  function symbolsMatchHub(a, b) {
    const x = normSym(a);
    const y = normSym(b);
    if (!x || !y) return false;
    return x === y;
  }

  function ordersCollapseKey(exchangeId, symbol) {
    const sym = normSym(symbol) || "unknown";
    return `hub_orders_${exchangeId}_${sym}`;
  }

  function isOrdersCollapseOpen(exchangeId, symbol) {
    return localStorage.getItem(ordersCollapseKey(exchangeId, symbol)) === "1";
  }

  function condOrderRole(o) {
    const lb = (o && o.label) || "";
    if (/止盈止损/.test(lb)) return null;
    if (/止损/.test(lb)) return "sl";
    if (/止盈/.test(lb)) return "tp";
    return null;
  }

  function dedupeCondOrdersByRole(orders) {
    const list = Array.isArray(orders) ? orders : [];
    const byRole = {};
    const others = [];
    for (const o of list) {
      const role = condOrderRole(o);
      if (role) byRole[role] = o;
      else others.push(o);
    }
    const out = others.slice();
    if (byRole.tp) out.push(byRole.tp);
    if (byRole.sl) out.push(byRole.sl);
    return out;
  }

  function dedupeCondOrdersByTrigger(orders) {
    const list = Array.isArray(orders) ? orders : [];
    const seen = new Set();
    const out = [];
    for (const o of list) {
      const px = orderTriggerOrPrice(o);
      const key =
        px != null
          ? "t:" + String(px)
          : o && o.id
            ? "id:" + String(o.id)
            : null;
      if (key && seen.has(key)) continue;
      if (key) seen.add(key);
      out.push(o);
    }
    return out;
  }

  function upsertExTpslCondOrder(cond, role, slot) {
    if (!slot || slot.trigger_price == null || slot.trigger_price === "") return;
    const label = role === "sl" ? "止损" : "止盈";
    const item = {
      label: label,
      trigger_price: Number(slot.trigger_price),
      amount: slot.amount != null ? slot.amount : null,
      id: slot.order_id || "",
      channel: "algo",
    };
    const idx = cond.findIndex(function (o) {
      const lb = o.label || "";
      return role === "sl" ? /^止损\b/.test(lb) || lb.includes("止损") : /^止盈\b/.test(lb) || lb.includes("止盈");
    });
    if (idx >= 0) cond[idx] = Object.assign({}, cond[idx], item);
    else cond.push(item);
  }

  function condOrdersFromPosition(pos) {
    let cond = dedupeCondOrdersByRole(
      Array.isArray(pos.conditional_orders) ? pos.conditional_orders : []
    );
    cond = dedupeCondOrdersByTrigger(cond);
    const et = pos.exchange_tpsl;
    if (!et) return cond;
    upsertExTpslCondOrder(cond, "sl", et.sl);
    upsertExTpslCondOrder(cond, "tp", et.tp);
    return cond;
  }

  function findMonitorOrder(orders, symbol, side) {
    const want = (side || "").toLowerCase();
    for (const o of orders || []) {
      const sym = o.exchange_symbol || o.symbol || "";
      if (!symbolsMatchHub(sym, symbol)) continue;
      const d = (o.direction || "").toLowerCase();
      if (!d || d === want) return o;
    }
    return null;
  }

  function calcRrRatio(side, entry, sl, tp) {
    const e = Number(entry);
    const s = Number(sl);
    const t = Number(tp);
    if (![e, s, t].every((n) => Number.isFinite(n) && n > 0)) return null;
    if ((side || "long").toLowerCase() === "short") {
      const risk = s - e;
      const reward = e - t;
      if (risk <= 0 || reward <= 0) return null;
      return reward / risk;
    }
    const risk = e - s;
    const reward = t - e;
    if (risk <= 0 || reward <= 0) return null;
    return reward / risk;
  }

  function resolveTrendPlanRr(trendPlan, side, entry, sl, tp) {
    const t = trendPlan || {};
    if (t.money_rr != null && t.money_rr !== "") {
      const n = Number(t.money_rr);
      if (Number.isFinite(n) && n > 0) return n;
    }
    if (t.planned_rr != null && t.planned_rr !== "") {
      const n = Number(t.planned_rr);
      if (Number.isFinite(n) && n > 0) return n;
    }
    const e = t.avg_entry_price != null && t.avg_entry_price !== "" ? t.avg_entry_price : entry;
    const s = t.stop_loss != null && t.stop_loss !== "" ? t.stop_loss : sl;
    const p = t.take_profit != null && t.take_profit !== "" ? t.take_profit : tp;
    return calcRrRatio(side, e, s, p);
  }

  function resolveSnapshotRr(mo, side, entry, sl, tp, tpMonitored, trendPlan) {
    if (tpMonitored && isTrendContext(mo, trendPlan)) {
      const rr = resolveTrendPlanRr(trendPlan, side, entry, sl, tp);
      if (rr != null) return rr;
    }
    if (tpMonitored) return null;
    const snap = mo && mo.rr_ratio;
    if (snap != null && snap !== "") {
      const n = Number(snap);
      if (Number.isFinite(n)) return n;
    }
    const initSl = mo && (mo.initial_stop_loss != null ? mo.initial_stop_loss : mo.stop_loss);
    return calcRrRatio(side, entry, initSl || sl, tp);
  }

  function formatTpCellValue(tp, tpMonitored, symbol, tickMap) {
    if (tpMonitored) {
      if (tp != null && tp !== "") {
        return `程序监控 · ${fmtSymbolPrice(tp, symbol, tickMap)}`;
      }
      return "程序监控";
    }
    if (tp != null && tp !== "") return fmtSymbolPrice(tp, symbol, tickMap);
    return "—";
  }

  function isBreakevenSecured(side, entry, monitorOrder, cond, pos) {
    const mo = monitorOrder || {};
    const p = pos || {};
    const { sl } = pickExTpslOrders(cond);
    const trig = sl && sl.trigger_price != null ? Number(sl.trigger_price) : NaN;
    const liveEntry =
      p.entry_price != null && p.entry_price !== ""
        ? Number(p.entry_price)
        : mo.avg_entry_price != null && mo.avg_entry_price !== ""
          ? Number(mo.avg_entry_price)
          : Number(entry);
    if (Number.isFinite(trig) && Number.isFinite(liveEntry)) {
      if ((side || "long").toLowerCase() === "short") return trig <= liveEntry;
      return trig >= liveEntry;
    }
    if (mo.sl_breakeven_secured === true || mo.sl_breakeven_secured === 1) return true;
    if (p.sl_breakeven_secured === true || p.sl_breakeven_secured === 1) return true;
    return false;
  }

  function breakevenBadgeHtml() {
    return `<span class="pos-breakeven-badge">已保本</span>`;
  }

  async function fetchMonitorBoardSnapshot(opts) {
    const options = opts || {};
    const background = !!options.background;
    const showLoading = !!options.showLoading && !lastMonitorRows.length;
    const box = document.getElementById("monitor-grid");
    if (monitorBoardInFlight) {
      if (background) monitorBoardFetchPending = true;
      else return;
    }
    if (showLoading && box) {
      box.innerHTML =
        '<div class="board-loading"><span class="board-loading-spin" aria-hidden="true"></span>正在加载监控快照…<p class="board-loading-sub"></p></div>';
      scheduleMonitorBoardSlowHint(box);
    } else if (background && lastMonitorRows.length) {
      applyMonitorBoardUi(lastMonitorRows, null, { stale: true });
    }
    monitorBoardInFlight = true;
    const ctrl = new AbortController();
    const fetchTimer = setTimeout(() => ctrl.abort(), HUB_MONITOR_SNAPSHOT_TIMEOUT_MS);
    try {
      const r = await apiFetch(MONITOR_BOARD_SNAPSHOT_URL, { signal: ctrl.signal });
      const data = await r.json();
      if (!r.ok) {
        throw new Error(data.msg || data.detail || `HTTP ${r.status}`);
      }
      const ver = Number(data.board_version) || 0;
      const rows = data.rows || [];
      const waitingFirst = data.aggregating && !rows.length && ver <= localBoardVersion;
      if (waitingFirst && showLoading) {
        if (box) {
          const sub = box.querySelector(".board-loading-sub");
          if (sub) sub.textContent = "后台正在首次聚合三所数据(约 5～15 秒)…";
        }
        return;
      }
      const ts = data.updated_at || "";
      const versionChanged = ver !== localBoardVersion;
      const timeChanged = ts && ts !== lastMonitorBoardUpdatedAt;
      if (versionChanged || timeChanged || !lastMonitorRows.length) {
        localBoardVersion = ver;
        lastMonitorRows = rows;
        lastMonitorTotals = data.totals || null;
        saveMonitorBoardCache(lastMonitorRows, ts, ver, lastMonitorTotals);
        applyMonitorBoardUi(lastMonitorRows, ts, {
          stale: !!data.aggregating,
        });
      } else if (data.aggregating && lastMonitorRows.length) {
        applyMonitorBoardUi(lastMonitorRows, data.updated_at || lastMonitorBoardUpdatedAt, {
          stale: true,
        });
      }
      if (data.ok === false && data.msg && !background) {
        showToast(String(data.msg), true);
      }
    } catch (e) {
      const msg =
        e && e.name === "AbortError" ? "读取监控快照超时,请检查中控是否运行" : String(e);
      if (background && lastMonitorRows.length) {
        showToast("快照读取失败,仍显示上次数据", true);
        applyMonitorBoardUi(lastMonitorRows, null, { stale: false });
        return;
      }
      if (box) box.innerHTML = `<div class="err">${esc(msg)}</div>`;
    } finally {
      clearTimeout(fetchTimer);
      clearMonitorBoardSlowHint();
      monitorBoardInFlight = false;
      if (monitorBoardFetchPending) {
        monitorBoardFetchPending = false;
        void fetchMonitorBoardSnapshot({ background: true });
      }
    }
  }

  async function refreshMonitorBoardNow() {
    if (lastMonitorRows.length) {
      applyMonitorBoardUi(lastMonitorRows, lastMonitorBoardUpdatedAt, { stale: true });
    }
    try {
      await requestMonitorBoardRefresh();
      await fetchMonitorBoardSnapshot({ background: false });
    } catch (e) {
      showToast(String(e), true);
    }
  }

  function closeExchangeFullscreen() {
    expandedExchangeId = "";
    sessionStorage.removeItem("hub_expanded_ex");
    const fs = document.getElementById("exchange-fullscreen");
    if (fs) {
      fs.classList.add("hidden");
      fs.setAttribute("aria-hidden", "true");
    }
    document.body.classList.remove("hub-fullscreen-open");
  }

  function openExchangeFullscreen(exId) {
    expandedExchangeId = String(exId);
    sessionStorage.setItem("hub_expanded_ex", expandedExchangeId);
    renderMonitorGrid(lastMonitorRows);
  }

  function pnlSigned(v, decimals) {
    const n = Number(v);
    const d = decimals == null ? 2 : decimals;
    if (!Number.isFinite(n)) return "—";
    if (Math.abs(n) < 1e-12) return fmt(0, d);
    const abs = fmt(Math.abs(n), d);
    return (n > 0 ? "+" : "-") + abs;
  }

  const MONITOR_STATS_FOLD_KEY = "hub_monitor_stats_collapsed";

  function isMonitorStatsCollapsed() {
    try {
      const v = localStorage.getItem(MONITOR_STATS_FOLD_KEY);
      if (v === null || v === "") return true;
      return v === "1";
    } catch (_e) {
      return true;
    }
  }

  function setMonitorStatsCollapsed(collapsed) {
    try {
      localStorage.setItem(MONITOR_STATS_FOLD_KEY, collapsed ? "1" : "0");
    } catch (_e) {
      /* ignore */
    }
  }

  function renderMonitorStatsCard(totals) {
    const t = totals || {};
    const day = t.trading_day || "—";
    const resetH = t.reset_hour != null ? t.reset_hour : 8;
    const winN = Number(t.win_count) || 0;
    const lossN = Number(t.loss_count) || 0;
    const floatVal = Number(t.float_pnl_u);
    const collapsed = isMonitorStatsCollapsed();
    function cell(label, main, sub, valCls) {
      return `<div class="monitor-stat-cell">
        <div class="monitor-stat-label">${esc(label)}</div>
        <div class="monitor-stat-value ${valCls || ""}">${main}</div>
        ${sub ? `<div class="monitor-stat-sub">${sub}</div>` : ""}
      </div>`;
    }
    const winSub =
      winN > 0 && Number.isFinite(Number(t.win_pnl_u))
        ? `<span class="${pnlCls(t.win_pnl_u)}">${esc(pnlSigned(t.win_pnl_u, 2))}U</span>`
        : "—";
    const lossSub =
      lossN > 0 && Number.isFinite(Number(t.loss_pnl_u))
        ? `<span class="${pnlCls(t.loss_pnl_u)}">${esc(pnlSigned(t.loss_pnl_u, 2))}U</span>`
        : "—";
    const floatMain = esc(pnlSigned(floatVal, 2)) + "U";
    const floatCls = Math.abs(floatVal) > 1e-9 ? pnlCls(floatVal) : "";
    const foldLabel = collapsed ? "展开明细" : "收起";
    return `<div class="card card-online monitor-stats-card${collapsed ? " is-collapsed" : ""}" data-monitor-stats="1">
      <div class="card-head monitor-stats-head">
        <div class="monitor-stats-head-main">
          <div class="card-title-row">
            <div class="card-title">今日统计</div>
            <button type="button" class="ghost btn-monitor-stats-toggle" aria-expanded="${collapsed ? "false" : "true"}">${foldLabel}</button>
          </div>
          <div class="card-sub">交易日 ${esc(day)} · 北京时间 ${esc(String(resetH))}:00 切日</div>
        </div>
        <div class="monitor-stats-float-summary">
          <div class="monitor-stat-label">总浮盈亏</div>
          <div class="monitor-stat-value monitor-stats-float-value ${floatCls}">${floatMain}</div>
        </div>
      </div>
      <div class="card-body monitor-stats-detail">
        <div class="monitor-stats-grid">
          ${cell("今日开仓", String(Number(t.open_count) || 0), "含未平", "")}
          ${cell("今日平仓", String(Number(t.closed_count) || 0), "", "")}
          ${cell("持有仓位", String(Number(t.open_position_count) || 0), "", "")}
          ${cell("盈利", String(winN), winSub, winN > 0 ? "pnl-pos" : "")}
          ${cell("亏损", String(lossN), lossSub, lossN > 0 ? "pnl-neg" : "")}
          ${cell("总浮盈亏", floatMain, "", floatCls)}
        </div>
      </div>
    </div>`;
  }

  function renderMonitorGrid(rows) {
    const box = document.getElementById("monitor-grid");
    const fs = document.getElementById("exchange-fullscreen");
    const fsInner = document.getElementById("exchange-fullscreen-inner");
    if (!box) return;
    if (expandedExchangeId && !rows.some((r) => String(r.id) === String(expandedExchangeId))) {
      closeExchangeFullscreen();
    }
    const mobileTiles = isMobileLayout() && !expandedExchangeId;
    const displayRows = mobileTiles ? sortRowsForMobileDashboard(rows) : rows;
    const optionsSplit = monitorOptionsSplitActive(displayRows);
    monitorGridOptionsSplit = optionsSplit;
    const showStatsCard = !expandedExchangeId;
    box.classList.toggle("grid-monitor-tiles", mobileTiles);
    box.classList.toggle("grid-monitor-2x2", showStatsCard && !mobileTiles && !optionsSplit);
    box.classList.toggle("grid-monitor-with-stats", showStatsCard && mobileTiles);
    box.classList.toggle("grid-monitor-options-split", optionsSplit && showStatsCard);
    try {
      const statsHtml = showStatsCard ? renderMonitorStatsCard(lastMonitorTotals) : "";
      let cardsHtml = "";
      if (optionsSplit) {
        const okxRow = displayRows.find((r) => rowHasOptionsLayout(r));
        const otherRows = displayRows.filter((r) => !rowHasOptionsLayout(r));
        const ph =
          '<div class="card card-monitor-split-side card-monitor-placeholder" aria-hidden="true"></div>';
        /* 平铺 2×2 顺序:永续|币安 / 期权|Gate —— 同行左右同高,多仓时该行一起长高 */
        const cells = [
          okxRow ? renderMonitorCard(okxRow, { okxPart: "perp", splitSide: true }) : ph,
          otherRows[0] ? renderMonitorCard(otherRows[0], { splitSide: true }) : ph,
          okxRow ? renderMonitorCard(okxRow, { okxPart: "options", splitSide: true }) : ph,
          otherRows[1] ? renderMonitorCard(otherRows[1], { splitSide: true }) : ph,
        ];
        for (let i = 2; i < otherRows.length; i++) {
          cells.push(renderMonitorCard(otherRows[i], { splitSide: true }));
        }
        cardsHtml = `<div class="monitor-split-body monitor-split-2x2">${cells.join("")}</div>`;
      } else {
        cardsHtml =
          displayRows
            .map((r) => (mobileTiles ? renderMonitorTile(r) : renderMonitorCard(r)))
            .join("") || (showStatsCard ? "" : '<div class="err">无已启用账户</div>');
      }
      box.innerHTML = statsHtml + cardsHtml;
      if (showStatsCard && !cardsHtml && !statsHtml) {
        box.innerHTML = '<div class="err">无已启用账户</div>';
      }
    } catch (err) {
      console.error("renderMonitorGrid", err);
      box.innerHTML = `<div class="err">监控区渲染失败:${esc(String(err && err.message ? err.message : err))}</div>`;
    }
    syncMonitorGridColumns(box, displayRows.length + (showStatsCard ? 1 : 0), {
      statsFirst: showStatsCard && !optionsSplit,
      optionsSplit: optionsSplit && showStatsCard,
    });
    bindMonitorInteractions(box);
    if (window.TimeCloseUI && TimeCloseUI.tickLocalCountdowns) {
      TimeCloseUI.tickLocalCountdowns();
    }
    ensureHubHoldDurationTimer();
    if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
      OptionsExpiryCountdown.ensureTimer();
    }

    if (expandedExchangeId && fs && fsInner) {
      const row = rows.find((r) => String(r.id) === String(expandedExchangeId));
      if (row) {
        try {
          fsInner.innerHTML = renderFullscreenExchange(row);
          fs.classList.remove("hidden");
          fs.setAttribute("aria-hidden", "false");
          document.body.classList.add("hub-fullscreen-open");
          bindMonitorInteractions(fsInner);
          if (window.TimeCloseUI && TimeCloseUI.tickLocalCountdowns) {
            TimeCloseUI.tickLocalCountdowns();
          }
          ensureHubHoldDurationTimer();
          if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
            OptionsExpiryCountdown.ensureTimer();
          }
          fsInner.querySelectorAll(".btn-expand-back").forEach((btn) => {
            btn.onclick = (ev) => {
              ev.stopPropagation();
              closeExchangeFullscreen();
              renderMonitorGrid(lastMonitorRows);
            };
          });
        } catch (err) {
          console.error("renderFullscreenExchange", err);
          closeExchangeFullscreen();
          showToast("全屏渲染失败: " + err, true);
        }
      } else {
        closeExchangeFullscreen();
      }
    } else {
      closeExchangeFullscreen();
    }
  }

  function normalizeMarketSymbol(raw) {
    let s = (raw || "").trim().toUpperCase();
    if (!s) return "";
    if (s.includes(":")) {
      const base = s.split(":")[0];
      if (base.includes("/")) return base;
    }
    return s;
  }

  function resolveExchangeKey(exchangeId) {
    const row = (lastMonitorRows || []).find((r) => String(r.id) === String(exchangeId));
    return (row && (row.key || row.id)) || exchangeId;
  }

  function findTrendPlan(trends, symbol, side) {
    const want = (side || "").toLowerCase();
    for (const t of trends || []) {
      const sym = t.symbol || t.exchange_symbol || "";
      if (!symbolsMatchHub(sym, symbol)) continue;
      const d = (t.direction || "").toLowerCase();
      if (!d || d === want) return t;
    }
    return null;
  }

  function orderTriggerOrPrice(o) {
    if (!o) return null;
    if (o.trigger_price != null && o.trigger_price !== "") {
      const t = Number(o.trigger_price);
      if (Number.isFinite(t) && t > 0) return t;
    }
    if (o.price != null && o.price !== "") {
      const p = Number(o.price);
      if (Number.isFinite(p) && p > 0) return p;
    }
    return null;
  }

  function inferTpslFromCondOrders(side, cond, entry) {
    const picked = pickExTpslOrders(cond);
    let sl = picked.sl ? orderTriggerOrPrice(picked.sl) : "";
    let tp = picked.tp ? orderTriggerOrPrice(picked.tp) : "";
    if (sl !== "" && sl != null) sl = Number(sl);
    if (tp !== "" && tp != null) tp = Number(tp);
    if (sl !== "" && tp !== "" && Number(sl) !== Number(tp)) {
      return { sl, tp };
    }

    const triggers = (cond || [])
      .map(function (o) {
        const px = orderTriggerOrPrice(o);
        return px == null ? null : { price: px, label: o.label || "" };
      })
      .filter(function (o) {
        return o != null;
      });
    if (!triggers.length) return { sl: sl || "", tp: tp || "" };

    const s = (side || "long").toLowerCase();
    const e = entry != null && Number.isFinite(Number(entry)) ? Number(entry) : null;

    if (e != null) {
      const below = triggers.filter(function (t) {
        return t.price < e;
      });
      const above = triggers.filter(function (t) {
        return t.price > e;
      });
      if (s === "long") {
        if (sl === "" && below.length) {
          sl = Math.max.apply(
            null,
            below.map(function (t) {
              return t.price;
            })
          );
        }
        if (tp === "" && above.length) {
          tp = Math.min.apply(
            null,
            above.map(function (t) {
              return t.price;
            })
          );
        }
      } else {
        if (sl === "" && above.length) {
          sl = Math.min.apply(
            null,
            above.map(function (t) {
              return t.price;
            })
          );
        }
        if (tp === "" && below.length) {
          tp = Math.max.apply(
            null,
            below.map(function (t) {
              return t.price;
            })
          );
        }
      }
    }

    if (triggers.length === 1 && sl === "" && tp === "") {
      const one = triggers[0];
      const p = one.price;
      const lbl = one.label;
      if (e != null) {
        if (s === "long") {
          if (p < e) sl = p;
          else if (p > e) tp = p;
        } else if (p > e) sl = p;
        else if (p < e) tp = p;
      } else if (/止损/.test(lbl)) sl = p;
      else if (/止盈/.test(lbl) && !/止盈止损/.test(lbl)) tp = p;
    }

    if (sl !== "" && tp !== "" && Number(sl) === Number(tp)) tp = "";
    return { sl: sl || "", tp: tp || "" };
  }

  function resolvePositionTpsl(pos, monitorOrder, trendPlan) {
    const mo = monitorOrder || {};
    const tp = trendPlan || {};
    const cond = condOrdersFromPosition(pos);
    const entryRaw =
      pos.entry_price != null
        ? pos.entry_price
        : mo.trigger_price != null
          ? mo.trigger_price
          : tp.avg_entry_price;
    const entryN = entryRaw != null && entryRaw !== "" ? Number(entryRaw) : null;
    const isTrend = isTrendContext(mo, trendPlan);
    const handoff = isTrendHandoffOrder(mo);

    let sl = mo.stop_loss != null && mo.stop_loss !== "" ? mo.stop_loss : "";
    let takeProfit = mo.take_profit != null && mo.take_profit !== "" ? mo.take_profit : "";
    let tpMonitored = false;

    if (handoff) {
      tpMonitored = false;
    } else if (isTrend) {
      tpMonitored = true;
      if (trendPlan && trendPlan.stop_loss != null && trendPlan.stop_loss !== "") {
        sl = trendPlan.stop_loss;
      }
      if (trendPlan && trendPlan.take_profit != null && trendPlan.take_profit !== "") {
        takeProfit = trendPlan.take_profit;
      } else {
        takeProfit = "";
      }
    }

    const inferred = inferTpslFromCondOrders(pos.side, cond, entryN);
    if (inferred.sl !== "" && inferred.sl != null) {
      sl = inferred.sl;
    } else if (sl === "" || sl == null) {
      sl = inferred.sl;
    }
    if (!tpMonitored) {
      if (inferred.tp !== "" && inferred.tp != null) {
        takeProfit = inferred.tp;
      } else if (takeProfit === "" || takeProfit == null) {
        takeProfit = inferred.tp;
      }
    }

    if (sl !== "" && takeProfit !== "" && Number(sl) === Number(takeProfit)) {
      takeProfit = "";
    }

    return {
      entry: entryRaw,
      sl,
      tp: takeProfit,
      tp_monitored: tpMonitored,
      is_trend: isTrend,
      is_handoff: handoff,
    };
  }

  function buildPositionMarketContext(pos, monitorOrder, trendPlan, exchangeId) {
    const mo = monitorOrder || {};
    const tpsl = resolvePositionTpsl(pos, monitorOrder, trendPlan);
    const cond = condOrdersFromPosition(pos);
    const reg = Array.isArray(pos.regular_orders) ? pos.regular_orders : [];
    const num = function (v) {
      if (v == null || v === "") return null;
      const n = Number(v);
      return Number.isFinite(n) ? n : null;
    };
    const orders = [];
    cond.forEach(function (o) {
      orders.push({
        kind: "条件",
        label: o.label || "条件单",
        price: num(o.trigger_price),
        amount: num(o.amount),
      });
    });
    reg.forEach(function (o) {
      orders.push({
        kind: "普通",
        label: o.label || o.type || "委托",
        price: num(o.price != null ? o.price : o.trigger_price),
        amount: num(o.amount),
      });
    });
    const entryPx = num(pos.entry_price != null ? pos.entry_price : tpsl.entry);
    const markPx = num(pos.mark_price);
    const contractSize = num(pos.contract_size);
    const upnl = resolvePositionUpnlUsdt(pos, trendPlan, markPx);
    const planMargin =
      trendPlan && trendPlan.plan_margin_capital != null
        ? num(trendPlan.plan_margin_capital)
        : mo.margin_capital != null
          ? num(mo.margin_capital)
          : null;
    const leverage =
      trendPlan && trendPlan.leverage != null
        ? num(trendPlan.leverage)
        : mo.leverage != null
          ? num(mo.leverage)
          : null;
    return {
      exchange_id: exchangeId || null,
      symbol: (pos.symbol || "").trim(),
      side: (pos.side || "long").toLowerCase(),
      entry: entryPx,
      mark_price: markPx,
      stop_loss: num(tpsl.sl),
      take_profit: num(tpsl.tp),
      tp_monitored: !!tpsl.tp_monitored,
      is_trend: !!tpsl.is_trend,
      contracts: num(pos.contracts),
      contract_size: contractSize != null ? contractSize : 1,
      unrealized_pnl: upnl != null ? Number(upnl) : null,
      notional_usdt: num(pos.notional_usdt),
      plan_margin: planMargin,
      leverage: leverage,
      orders: orders,
    };
  }

  const HUB_MARKET_POS_CTX_KEY = "hubMarketPosContext";

  function encodePosCtx(ctx) {
    try {
      return btoa(unescape(encodeURIComponent(JSON.stringify(ctx))));
    } catch (e) {
      return "";
    }
  }

  function decodePosCtx(raw) {
    if (!raw) return null;
    try {
      return JSON.parse(decodeURIComponent(escape(atob(raw))));
    } catch (e) {
      return null;
    }
  }

  function marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan) {
    const symAttr = esc(symbol || "").replace(/"/g, "&quot;");
    const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, "&quot;");
    const ctxEnc = esc(
      encodePosCtx(buildPositionMarketContext(pos, monitorOrder, trendPlan, exchangeId))
    ).replace(
      /"/g,
      "&quot;"
    );
    return (
      'data-ex-id="' +
      esc(exchangeId) +
      '" data-ex-key="' +
      exKeyAttr +
      '" data-symbol="' +
      symAttr +
      '" data-pos-ctx="' +
      ctxEnc +
      '"'
    );
  }

  function openMarketForPosition(exchangeId, symbol, exchangeKey, posCtxRaw) {
    const exKey = exchangeKey || resolveExchangeKey(exchangeId);
    const sym = normalizeMarketSymbol(symbol);
    if (!exKey || !sym) {
      showToast("无法打开行情:缺少交易所或合约", true);
      return;
    }
    const ctx = decodePosCtx(posCtxRaw);
    if (ctx) {
      ctx.symbol = sym;
      ctx.exchange_key = exKey;
      sessionStorage.setItem(HUB_MARKET_POS_CTX_KEY, JSON.stringify(ctx));
    } else {
      sessionStorage.removeItem(HUB_MARKET_POS_CTX_KEY);
    }
    if (expandedExchangeId) {
      closeExchangeFullscreen();
    }
    const qs = new URLSearchParams({ exchange_key: exKey, symbol: sym });
    history.pushState({}, "", "/market?" + qs.toString());
    setActiveNav();
    if (window.hubMarketChart && window.hubMarketChart.openWith) {
      window.hubMarketChart.openWith(exKey, sym);
    }
  }

  function bindMonitorInteractions(box) {
    box.querySelectorAll(".btn-monitor-stats-toggle").forEach((btn) => {
      btn.onclick = (ev) => {
        ev.preventDefault();
        ev.stopPropagation();
        const next = !isMonitorStatsCollapsed();
        setMonitorStatsCollapsed(next);
        if (lastMonitorRows) renderMonitorGrid(lastMonitorRows);
      };
    });
    box.querySelectorAll(".btn-open-market").forEach((btn) => {
      btn.onclick = (ev) => {
        ev.preventDefault();
        ev.stopPropagation();
        openMarketForPosition(btn.dataset.exId, btn.dataset.symbol, btn.dataset.exKey, btn.dataset.posCtx);
      };
    });
    box.querySelectorAll(".btn-open-instance").forEach((btn) => {
      btn.onclick = (ev) => {
        ev.preventDefault();
        ev.stopPropagation();
        const msg = (btn.dataset.confirm || "").trim();
        if (msg && !confirm(msg)) return;
        openInstance(btn.dataset.exId, btn.dataset.next || "/", {
          newTab: btn.dataset.newTab === "1" || ev.ctrlKey || ev.metaKey,
        });
      };
    });
    box.querySelectorAll(".btn-hub-trend-stop").forEach((btn) => {
      btn.onclick = (ev) => {
        ev.preventDefault();
        ev.stopPropagation();
        hubTrendPlanStop(btn.dataset.exId, btn.dataset.planId);
      };
    });
    box.querySelectorAll(".btn-hub-trend-be").forEach((btn) => {
      btn.onclick = (ev) => {
        ev.preventDefault();
        ev.stopPropagation();
        const card = btn.closest(".hub-trend-plan-card");
        const inp = card ? card.querySelector(".hub-plan-be-input") : null;
        hubTrendPlanBreakeven(btn.dataset.exId, btn.dataset.planId, inp);
      };
    });
    box.querySelectorAll(".btn-close-ex").forEach((btn) => {
      btn.onclick = () => closeOne(btn.dataset.id);
    });
    box.querySelectorAll(".btn-close-pos").forEach((btn) => {
      btn.onclick = (ev) => {
        ev.stopPropagation();
        closeOnePosition(btn.dataset.exId, btn.dataset.symbol, btn.dataset.side);
      };
    });
    box.querySelectorAll(".btn-cancel-order").forEach((btn) => {
      btn.onclick = (ev) => {
        ev.stopPropagation();
        cancelOneOrder(
          btn.dataset.exId,
          btn.dataset.symbol,
          btn.dataset.orderId,
          btn.dataset.channel
        );
      };
    });
    box.querySelectorAll(".btn-cancel-cond-all").forEach((btn) => {
      btn.onclick = (ev) => {
        ev.preventDefault();
        ev.stopPropagation();
        cancelSymbolOrders(btn.dataset.exId, btn.dataset.symbol, "conditional");
      };
    });
    box.querySelectorAll(".btn-place-tpsl").forEach((btn) => {
      btn.onclick = (ev) => {
        ev.stopPropagation();
        openTpslModal(
          btn.dataset.exId,
          btn.dataset.symbol,
          btn.dataset.side,
          btn.dataset.contracts,
          btn.dataset.sl || "",
          btn.dataset.tp || ""
        );
      };
    });
    box.querySelectorAll(".card-expand-zone").forEach((zone) => {
      zone.onclick = (ev) => {
        if (ev.target.closest("a, button, input, summary, details, .card-actions")) return;
        const id = zone.closest(".card")?.dataset.exId;
        if (id) openExchangeFullscreen(id);
      };
    });
    box.querySelectorAll("details.pos-orders-collapse[data-collapse-key]").forEach((el) => {
      el.addEventListener("toggle", () => {
        const k = el.dataset.collapseKey;
        if (k) localStorage.setItem(k, el.open ? "1" : "0");
      });
    });
  }

  function renderOrderRows(exchangeId, symbol, orders, kind, tickMap) {
    if (!orders || !orders.length) {
      const hint =
        kind === "conditional"
          ? "暂无条件单(止盈/止损等)"
          : "暂无普通委托";
      return `<div class="order-empty">${hint}</div>`;
    }
    const symAttr = esc(symbol || "").replace(/"/g, "&quot;");
    const rows = orders
      .map((o) => {
        const oidAttr = esc(o.id || "").replace(/"/g, "&quot;");
        const chAttr = esc(o.channel || "regular").replace(/"/g, "&quot;");
        const trig =
          o.trigger_price != null
            ? fmtSymbolPrice(o.trigger_price, symbol, tickMap)
            : o.price != null
              ? fmtSymbolPrice(o.price, symbol, tickMap)
              : "—";
        return `<tr>
          <td>${esc(o.label || o.type || "委托")}</td>
          <td>${fmt(o.amount, 4)}</td>
          <td>${trig}</td>
          <td class="td-actions"><button type="button" class="btn-cancel-order ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-order-id="${oidAttr}" data-channel="${chAttr}">撤单</button></td>
        </tr>`;
      })
      .join("");
    return `<table class="data-table data-table-sub"><thead><tr><th>类型</th><th>数量</th><th>触发/价格</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table>`;
  }

  function guessTpslFromCondOrders(side, cond, entry) {
    return inferTpslFromCondOrders(side, cond, entry);
  }

  function renderOrdersCollapse(exchangeId, symbol, cond, reg, tickMap) {
    const symAttr = esc(symbol || "").replace(/"/g, "&quot;");
    const orderTotal = cond.length + reg.length;
    const collapseKey = ordersCollapseKey(exchangeId, symbol);
    const openAttr = isOrdersCollapseOpen(exchangeId, symbol) ? " open" : "";
    const condAllBtn =
      cond.length > 0
        ? `<button type="button" class="btn-cancel-cond-all btn-sm ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}">撤销条件单</button>`
        : "";
    const condBody = renderOrderRows(exchangeId, symbol, cond, "conditional", tickMap);
    const regBody = renderOrderRows(exchangeId, symbol, reg, "limit", tickMap);
    return `<details class="pos-orders-collapse"${openAttr} data-collapse-key="${esc(collapseKey)}">
        <summary class="pos-orders-collapse-summary">
          <span class="pos-orders-collapse-label">委托单 <em>${orderTotal}</em></span>
          <span class="pos-orders-collapse-meta">条件 ${cond.length} · 普通 ${reg.length}</span>
          ${condAllBtn}
        </summary>
        <div class="pos-orders-collapse-body">
          <div class="orders-section">
            <div class="orders-section-head">条件单</div>
            ${condBody}
          </div>
          <div class="orders-section">
            <div class="orders-section-head">普通委托</div>
            ${regBody}
          </div>
        </div>
      </details>`;
  }

  function syntheticExTpslOrder(role, price, amount) {
    if (price == null || price === "" || !Number.isFinite(Number(price))) return null;
    return {
      label: role === "sl" ? "止损" : "止盈",
      trigger_price: Number(price),
      price: Number(price),
      amount: amount != null ? amount : null,
      id: "",
      channel: "plan",
    };
  }

  function pickExTpslOrders(cond) {
    let sl = cond.find((o) => /^止损\b/.test(o.label || ""));
    let tp = cond.find((o) => /^止盈\b/.test(o.label || "") && !(o.label || "").includes("止盈止损"));
    if (!sl || !tp) {
      const combo = cond.find((o) => (o.label || "").includes("止盈止损"));
      if (combo) {
        const m = (combo.label || "").match(/SL=([\d.eE+-]+).*TP=([\d.eE+-]+)/i);
        if (m) {
          if (!sl) sl = { ...combo, label: "止损", trigger_price: Number(m[1]) };
          if (!tp) tp = { ...combo, label: "止盈", trigger_price: Number(m[2]) };
        }
      }
    }
    if (!sl) sl = cond.find((o) => (o.label || "").includes("止损"));
    if (!tp) tp = cond.find((o) => (o.label || "").includes("止盈") && o !== sl);
    return { sl, tp };
  }

  function renderExTpslRows(exchangeId, symbol, cond, tickMap, resolvedTpsl, contracts, intradayDiscipline) {
    const symAttr = esc(symbol || "").replace(/"/g, "&quot;");
    const intraday = !!intradayDiscipline;
    let { sl, tp } = pickExTpslOrders(cond);
    const plan = resolvedTpsl || {};
    if (!sl && plan.sl != null && plan.sl !== "") {
      sl = syntheticExTpslOrder("sl", plan.sl, contracts);
    }
    if (!tp && plan.tp != null && plan.tp !== "") {
      tp = syntheticExTpslOrder("tp", plan.tp, contracts);
    }
    function row(label, o) {
      if (!o) {
        return `<div class="pos-ex-order-row"><span class="pos-ex-order-main">${label}:—</span></div>`;
      }
      const oid = esc(o.id || "").replace(/"/g, "&quot;");
      const ch = esc(o.channel || "regular").replace(/"/g, "&quot;");
      const px = orderTriggerOrPrice(o);
      const trig = px != null ? fmtSymbolPrice(px, symbol, tickMap) : "—";
      const cancelBtn =
        !intraday && oid && o.channel !== "plan"
          ? `<button type="button" class="pos-ex-cancel-btn btn-cancel-order" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-order-id="${oid}" data-channel="${ch}">撤单</button>`
          : "";
      const planHint = o.channel === "plan" ? '<span class="pos-ex-plan-hint">(下单监控)</span>' : "";
      return `<div class="pos-ex-order-row">
        <span class="pos-ex-order-main">${label}:触发 ${trig} · 数量 ${fmt(o.amount, 4)}${planHint}</span>
        ${cancelBtn}
      </div>`;
    }
    return row("止损", sl) + row("止盈", tp);
  }

  function trendAddSummaryHtml(t, tickMap) {
    const done = t.add_count != null ? t.add_count : t.legs_done;
    const total = t.add_count_total != null ? t.add_count_total : t.dca_legs;
    const sym = t.exchange_symbol || t.symbol || "";
    let html = "";
    if (done != null && Number(done) >= 0) {
      html += total != null ? ` · 补仓 <strong>${esc(done)}/${esc(total)}</strong>` : ` · 补仓 <strong>${esc(done)}</strong> 次`;
      const pxs = t.add_prices_display;
      if (Array.isArray(pxs) && pxs.length) {
        html += ` · 加仓价 ${pxs.map((p) => esc(p)).join(" / ")}`;
      } else if (Array.isArray(t.add_prices) && t.add_prices.length) {
        html += ` · 加仓价 ${t.add_prices.map((p) => esc(fmtSymbolPrice(p, sym, tickMap))).join(" / ")}`;
      } else if (Number(done) === 0) {
        html += " · 加仓价 —";
      }
    }
    return html;
  }

  function timeCloseSymbolBadgeHtml(item) {
    if (!item || !item.time_close_enabled) return "";
    const tcLabel = item.time_close_label || `时间平仓 ${item.time_close_hours || ""}h`;
    const tcCd = item.time_close_countdown || "--:--:--";
    const tcAt = item.time_close_at_ms != null ? String(item.time_close_at_ms) : "";
    return (
      `<span class="pos-symbol-time-close pos-time-close-meta pos-meta-on" data-close-at-ms="${esc(tcAt)}">` +
      `${esc(tcLabel)} · <span class="pos-time-close-cd">${esc(tcCd)}</span></span>`
    );
  }

  function forceCloseSymbolBadgeHtml(item) {
    if (!item || !item.force_close_enabled) return "";
    const fcLabel = item.force_close_label || "强制清仓";
    const fcCd = item.force_close_countdown || "--:--:--";
    const fcAt = item.force_close_at_ms != null ? String(item.force_close_at_ms) : "";
    const fcActive = item.force_close_active ? "1" : "0";
    return (
      `<span class="pos-symbol-force-close pos-force-close-meta" data-force-close-at-ms="${esc(fcAt)}" data-force-close-active="${esc(fcActive)}">` +
      `${esc(fcLabel)} · <span class="pos-force-close-cd">${esc(fcCd)}</span></span>`
    );
  }

  function isIntradayDisciplineRow(row) {
    const m = row && row.meta;
    if (!m || typeof m !== "object") return false;
    if (m.intraday_discipline === true) return true;
    return m.order_entry_profile === "intraday";
  }

  function forceCloseHeadBadgeHtml(state) {
    if (!state || !state.enabled) return "";
    return forceCloseSymbolBadgeHtml({
      force_close_enabled: true,
      force_close_label: state.label || "强制清仓",
      force_close_countdown: state.countdown || "--:--:--",
      force_close_at_ms: state.next_at_ms,
      force_close_active: state.active,
    });
  }

  function renderTrendDcaTable(t, tickMap) {
    const levels = resolveTrendDcaLevels(t);
    if (!levels.length) return "";
    const sym = t.exchange_symbol || t.symbol || "";
    const rows = levels
      .map((lv) => {
        const price =
          lv.price != null && lv.price !== ""
            ? fmtSymbolPrice(lv.price, sym, tickMap)
            : "—";
        const amt =
          lv.contracts != null && lv.contracts !== "" ? esc(String(lv.contracts)) : "—";
        const avg =
          lv.avg_entry != null && lv.avg_entry !== ""
            ? fmtSymbolPrice(lv.avg_entry, sym, tickMap)
            : "—";
        const profitU =
          lv.profit_u != null && lv.profit_u !== "" ? fmt(lv.profit_u, 2) : "—";
        const riskU = lv.risk_u != null && lv.risk_u !== "" ? fmt(lv.risk_u, 2) : "—";
        const rr = lv.rr != null && lv.rr !== "" ? `${fmt(lv.rr, 2)}:1` : "—";
        const stCls = lv.status === "done" ? "st-done" : "st-pending";
        const label = lv.status_label || (lv.status === "done" ? "已补仓" : "待补仓");
        return `<tr>
          <td>${esc(lv.label || lv.leg_key || "—")}</td>
          <td>${esc(price)}</td>
          <td>${amt}</td>
          <td>${esc(avg)}</td>
          <td>${esc(profitU)}</td>
          <td>${esc(riskU)}</td>
          <td>${esc(rr)}</td>
          <td class="${stCls}">${esc(label)}</td>
        </tr>`;
      })
      .join("");
    return `<div class="plan-dca-block plan-dca-block--side">
      <div class="plan-dca-title">补仓计划明细</div>
      <table class="plan-dca-table">
        <tr><th>档位</th><th>触发价</th><th>张数</th><th>加仓后均价</th><th>止盈盈利(U)</th><th>止损(U)</th><th>盈亏比</th><th>状态</th></tr>
        ${rows}
      </table>
    </div>`;
  }

  function renderTrendPlanCard(t, tickMap, pos, exchangeRow) {
    const sym = t.exchange_symbol || t.symbol || "";
    const side = (t.direction || "long").toLowerCase();
    const sl = t.stop_loss_display || fmtSymbolPrice(t.stop_loss, sym, tickMap);
    const tp = t.take_profit_display || fmtSymbolPrice(t.take_profit, sym, tickMap);
    const avg = t.avg_entry_price_display || fmtSymbolPrice(t.avg_entry_price, sym, tickMap);
    const addZone =
      t.add_upper_display || fmtSymbolPrice(t.add_upper, sym, tickMap) || "—";
    const rr = resolveTrendPlanRr(t, side, t.avg_entry_price, t.stop_loss, t.take_profit);
    const rrTxt = rr != null ? `${fmt(rr, 2)}:1` : "—";
    const mark = resolveTrendMarkPrice(pos, t, sym, tickMap);
    const legsDone = t.add_count != null ? t.add_count : t.legs_done;
    const legsTotal = t.add_count_total != null ? t.add_count_total : t.dca_legs;
    const legsTxt =
      legsDone != null && legsTotal != null
        ? `${esc(legsDone)}/${esc(legsTotal)}`
        : legsDone != null
          ? esc(legsDone)
          : "—";
    const upnlTrend = resolveTrendFloatingPnl(pos, t);
    const pnlFmt = formatTrendPlanFloatingPnl(upnlTrend, t.plan_margin_capital);
    const pnlVal =
      pnlFmt.text === "—"
        ? "—"
        : `<span class="val ${pnlFmt.cls}">${esc(pnlFmt.text)}</span>`;
    const riskTxt =
      t.risk_percent != null && t.risk_percent !== "" ? `${esc(t.risk_percent)}%` : "—";
    const snapTxt =
      t.snapshot_available_usdt != null && t.snapshot_available_usdt !== ""
        ? `${fmt(t.snapshot_available_usdt, 2)}U`
        : "—";
    const marginTxt =
      t.plan_margin_capital != null && t.plan_margin_capital !== ""
        ? `≈${fmt(t.plan_margin_capital, 2)}U`
        : "—";
    const levTxt = t.leverage != null && t.leverage !== "" ? `${esc(t.leverage)}x` : "—";
    const bePctDefault =
      t.breakeven_default_offset_pct != null && t.breakeven_default_offset_pct !== ""
        ? t.breakeven_default_offset_pct
        : t.breakeven_offset_pct != null && t.breakeven_offset_pct !== ""
          ? t.breakeven_offset_pct
          : "0.3";
    const exId = exchangeRow && exchangeRow.id != null ? esc(exchangeRow.id) : "";
    const planId = esc(t.id);
    const caps = (exchangeRow && exchangeRow.capabilities) || [];
    const flaskOk =
      exchangeRow && exchangeRow.flask_ok !== false && (exchangeRow.hub_monitor || {}).ok !== false;
    const canHubTrend = !!(flaskOk && caps.includes("trend") && exId && planId);
    const beAppliedFlag = !!t.breakeven_applied;
    const endBtn = canHubTrend
      ? `<button type="button" class="btn-close-plan btn-hub-trend-stop" data-ex-id="${exId}" data-plan-id="${planId}">结束计划</button>`
      : "";
    const beBtn = canHubTrend && !beAppliedFlag
      ? `<button type="button" class="hub-plan-be-btn btn-hub-trend-be" data-ex-id="${exId}" data-plan-id="${planId}">保本移交下单监控</button>`
      : beAppliedFlag
        ? ""
        : `<span class="hub-plan-be-btn hub-plan-be-btn--static">保本移交下单监控</span>`;
    const beApplied =
      t.breakeven_applied
        ? `<span class="hub-plan-be-done">已保本 ${esc(String(t.breakeven_applied_at || "").slice(0, 16))}</span>`
        : "";
    const dcaHtml = renderTrendDcaTable(t, tickMap);
    const dcaCol = dcaHtml
      ? `<div class="hub-trend-plan-col hub-trend-plan-col-right">${dcaHtml}</div>`
      : `<div class="hub-trend-plan-col hub-trend-plan-col-right"><div class="plan-dca-block plan-dca-block--side plan-dca-block--empty"><div class="plan-dca-title">补仓计划明细</div><div class="hub-dca-empty">暂无补仓档位</div></div></div>`;
    return `<div class="plan-position-card hub-trend-plan-card">
      <div class="plan-card-head">
        <div class="plan-card-title">
          <span>#${esc(t.id)} ${esc(sym)}</span>
          ${renderDirectionBadge(t.direction)}
        </div>
        ${endBtn}
      </div>
      <div class="hub-trend-plan-body-cols">
        <div class="hub-trend-plan-col hub-trend-plan-col-left">
          <div class="plan-card-meta">
            来源: 趋势回调计划 ｜ 风险: ${riskTxt}
            ｜ <span class="accent">${esc(trendAddZoneLabel(t.direction))} ${esc(addZone)}</span>
            ｜ 已补仓 <strong>${legsTxt}</strong>
          </div>
          <div class="plan-card-grid">
            <div class="plan-cell"><span class="lbl">均价</span><span class="val">${esc(avg)}</span></div>
            <div class="plan-cell"><span class="lbl">止损</span><span class="val">${esc(sl)}</span></div>
            <div class="plan-cell"><span class="lbl">止盈</span><span class="val">${esc(tp)}</span></div>
            <div class="plan-cell"><span class="lbl">盈亏比</span><span class="val">${esc(rrTxt)}</span></div>
            <div class="plan-cell"><span class="lbl">标记价</span><span class="val">${esc(mark)}</span></div>
            <div class="plan-cell"><span class="lbl">浮盈亏</span>${pnlVal}</div>
          </div>
        </div>
        ${dcaCol}
      </div>
      <div class="hub-trend-plan-foot">
        <div class="plan-card-meta hub-plan-breakeven-row">
          <label class="hub-plan-be-label">
            保本移交 偏移%
            <input type="number" min="0" step="0.01" value="${esc(bePctDefault)}" class="hub-plan-be-input" data-ex-id="${exId}" data-plan-id="${planId}" ${canHubTrend && !beAppliedFlag ? "" : "disabled"} />
          </label>
          ${beBtn}
          ${beApplied}
        </div>
        <div class="plan-card-meta hub-plan-account-foot">
          快照可用: ${esc(snapTxt)} ｜ 计划保证金${esc(marginTxt)} ｜ 杠杆: ${levTxt}
        </div>
      </div>
    </div>`;
  }

  function renderTrendSection(trends, tickMap, positions, exchangeRow) {
    if (!trends || !trends.length) return "";
    const posList = Array.isArray(positions) ? positions : [];
    const cards = trends
      .map((t) => {
        const sym = t.exchange_symbol || t.symbol || "";
        const side = (t.direction || "long").toLowerCase();
        let matched = null;
        for (const p of posList) {
          if (!symbolsMatchHub(p.symbol, sym)) continue;
          const ps = (p.side || "").toLowerCase();
          if (!ps || ps === side) {
            matched = p;
            break;
          }
        }
        return renderTrendPlanCard(t, tickMap, matched, exchangeRow);
      })
      .join("");
    return `<div class="hub-trend-running">
      <div class="hub-trend-running-title">运行中的计划</div>
      <div class="running-plans-stack hub-trend-plan-list">${cards}</div>
    </div>`;
  }

  function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder, trendPlan, tickMap, intradayDiscipline) {
    const symbol = pos.symbol || "";
    const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, "&quot;");
    const side = (pos.side || "long").toLowerCase();
    const sideCn = sideDirLabel(side);
    const sideCls = sideDirCls(side) || "side-long";
    const mo = monitorOrder || {};
    const cond = condOrdersFromPosition(pos);
    const reg = Array.isArray(pos.regular_orders) ? pos.regular_orders : [];
    const tpsl = resolvePositionTpsl(pos, mo, trendPlan);
    const symAttr = esc(symbol).replace(/"/g, "&quot;");
    const sideAttr = esc(side).replace(/"/g, "&quot;");
    const contractsAttr = esc(String(pos.contracts != null ? pos.contracts : "")).replace(/"/g, "&quot;");
    const slAttr = esc(String(tpsl.sl)).replace(/"/g, "&quot;");
    const tpAttr = esc(String(tpsl.tp)).replace(/"/g, "&quot;");
    const entry = tpsl.entry;
    const sl = tpsl.sl;
    const tp = tpsl.tp;
    const tpMonitored = tpsl.tp_monitored;
    const isTrend = isTrendContext(mo, trendPlan);
    const intraday = !!intradayDiscipline;
    const rr = resolveSnapshotRr(mo, side, entry, sl, tp, tpMonitored, trendPlan);
    const beSecured = isBreakevenSecured(side, entry, mo, cond, pos);
    const upnl = resolveTrendFloatingPnl(pos, trendPlan);
    const pnlFmt = formatFloatingPnlText(upnl, pos.notional_usdt);
    const pnlText = pnlFmt.text;
    const sizingFoot = resolveTrendSizingFooter(mo, trendPlan, isTrend, pos);
    const openMeta = resolvePositionOpenMeta(mo, trendPlan, isTrend);
    const marginText =
      sizingFoot.margin != null && sizingFoot.margin !== "" && Number.isFinite(Number(sizingFoot.margin))
        ? fmt(Number(sizingFoot.margin), 2) + "U"
        : "—";
    const holdMsAttr =
      openMeta.openedAtMs != null && Number.isFinite(openMeta.openedAtMs)
        ? String(openMeta.openedAtMs)
        : "";
    const markDisplay = isTrend
      ? resolveTrendMarkPrice(pos, trendPlan, symbol, tickMap)
      : fmtMarkPrice(pos, tickMap);
    const meta = [];
    if (isTrend) {
      meta.push(monitorOrderSourceHtml(mo, trendPlan));
      const riskLine = formatMonitorRiskMeta(mo, trendPlan);
      if (riskLine) meta.push(riskLine);
      const latestRiskLine = formatLatestRiskMeta(mo, trendPlan, pos, tpsl);
      if (latestRiskLine) meta.push(latestRiskLine);
      if (trendPlan && trendPlan.id) {
        const zone =
          trendPlan.add_upper_display ||
          fmtSymbolPrice(trendPlan.add_upper, symbol, tickMap) ||
          "—";
        meta.push(
          `<span class="pos-meta-accent">${esc(trendAddZoneLabel(trendPlan.direction))} ${esc(zone)}</span>`
        );
        const addSum = trendAddSummaryHtml(trendPlan, tickMap);
        if (addSum) meta.push(addSum.replace(/^ · /, ""));
      }
      meta.push(`<span class="pos-meta-off">移动保本:关</span>`);
    } else if (mo.monitor_type || mo.key_signal_type || mo.trend_plan_id) {
      meta.push(monitorOrderSourceHtml(mo, trendPlan));
      meta.push(monitorEntryStyleHtml(mo, intraday));
      const riskLine = formatMonitorRiskMeta(mo, trendPlan);
      if (riskLine) meta.push(riskLine);
      const latestRiskLine = formatLatestRiskMeta(mo, trendPlan, pos, tpsl);
      if (latestRiskLine) meta.push(latestRiskLine);
      if (!intraday) {
        const beOn = mo.breakeven_enabled === 1 || mo.breakeven_enabled === true;
        meta.push(
          `<span class="${beOn ? "pos-meta-on" : "pos-meta-off"}">移动保本:${beOn ? "开" : "关"}</span>`
        );
      }
    } else {
      meta.push("来源: 交易所持仓");
      meta.push("风格: —");
      if (!intraday) meta.push(`<span class="pos-meta-off">移动保本:关</span>`);
    }
    const symBeBadge = beSecured ? ` ${breakevenBadgeHtml()}` : "";
    const tcSymBadge = !isTrend && mo.time_close_enabled ? timeCloseSymbolBadgeHtml(mo) : "";
    const fcSymBadge = !isTrend && mo.force_close_enabled ? forceCloseSymbolBadgeHtml(mo) : "";
    const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan);
    const headActions = intraday
      ? ""
      : `<div class="pos-head-actions">
          <button type="button" class="pos-entrust-btn btn-place-tpsl" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}" data-contracts="${contractsAttr}" data-sl="${slAttr}" data-tp="${tpAttr}">委托</button>
          <button type="button" class="pos-close-btn btn-close-pos" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}">平仓</button>
        </div>`;
    return `<div class="pos-card hub-pos-card">
      <div class="pos-card-head">
        <div class="pos-card-symbol">
          <button type="button" class="btn-open-market sym-link pos-symbol-link" ${mktAttrs} title="打开行情区(含入场/止盈止损)"><strong>${esc(symbol)}</strong></button>${tcSymBadge}${fcSymBadge}${symBeBadge}
          <span class="pos-side-badge ${sideCls}">${sideCn}</span>
        </div>
        ${headActions}
      </div>
      <div class="pos-meta">${meta.map((m) => `<span class="pos-meta-item">${m}</span>`).join("")}</div>
      <div class="pos-grid">
        <div class="pos-cell"><span class="pos-label">开仓价</span><span class="pos-value">${fmtEntryPrice(pos, tickMap)}</span></div>
        <div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">${markDisplay}</span></div>
        <div class="pos-cell"><span class="pos-label">止损</span><span class="pos-value">${sl != null && sl !== "" ? fmtSymbolPrice(sl, symbol, tickMap) : "—"}</span></div>
        <div class="pos-cell"><span class="pos-label">止盈</span><span class="pos-value${tpMonitored ? " pos-tp-program" : ""}">${formatTpCellValue(tp, tpMonitored, symbol, tickMap)}</span></div>
        <div class="pos-cell"><span class="pos-label">盈亏比</span><span class="pos-value">${rr != null ? fmt(rr, 2) + ":1" : "—"}</span></div>
        <div class="pos-cell"><span class="pos-label">张数</span><span class="pos-value">${fmt(pos.contracts, 2)}</span></div>
        <div class="pos-cell"><span class="pos-label">盈利金额</span><span class="pos-value">${formatTpProfitCell(mo, pos)}</span></div>
        ${
          showAccountPnlPref()
            ? `<div class="pos-cell"><span class="pos-label">浮盈亏</span><span class="pos-value ${pnlFmt.cls}">${pnlText}</span></div>`
            : ""
        }
      </div>
      <div class="pos-footer">
        <span>保证金: ${marginText}</span>
        <span>计划基数: ${sizingFoot.planBase != null && sizingFoot.planBase !== "" ? fmt(sizingFoot.planBase, 2) + "U" : "—"}</span>
        <span>杠杆: ${sizingFoot.leverage != null && sizingFoot.leverage !== "" ? esc(sizingFoot.leverage) + "x" : "—"}</span>
        <span>仓位占比: ${sizingFoot.positionRatio != null && sizingFoot.positionRatio !== "" ? fmt(sizingFoot.positionRatio, 2) + "%" : "—"}</span>
        <span>开仓时间: ${esc(openMeta.openedAtDisplay)}</span>
        <span>持仓时长: <span class="pos-hold-duration" data-opened-ms="${esc(holdMsAttr)}">${esc(formatLiveHoldDuration(openMeta.openedAtMs))}</span></span>
      </div>
      <div class="pos-ex-orders">
        <div class="pos-ex-orders-title">交易所止盈止损</div>
        ${renderExTpslRows(exchangeId, symbol, cond, tickMap, tpsl, pos.contracts, intraday)}
      </div>
      ${renderOrdersCollapse(exchangeId, symbol, cond, reg, tickMap)}
    </div>`;
  }

  function renderHubSectionCard(title, bodyHtml, emptyHint) {
    const inner = bodyHtml || `<div class="pos-empty">${esc(emptyHint || "暂无")}</div>`;
    return `<div class="hub-section-card">
      <div class="hub-section-head">${esc(title)}</div>
      <div class="hub-section-body">${inner}</div>
    </div>`;
  }

  function renderKeySection(keys, kmap) {
    if (!keys.length) return "";
    const cards = keys
      .map((k) => {
        const kp = kmap[k.id] || kmap[String(k.id)] || {};
        const mt = k.monitor_type || k.type || "";
        const pending = keyHasPendingOrder(k, kp);
        const cardCls = pending ? "hub-mini-card hub-key-pending" : "hub-mini-card";
        const dir = k.direction ? ` · ${renderDirectionHtml(k.direction)}` : "";
        const pendingTag = pending
          ? `<span class="hub-key-pending-tag">挂单中</span>`
          : "";
        const amtTxt = fmtKeyOrderAmount(k);
        const amtLine = amtTxt
          ? `<div class="hub-mini-line">挂单数量 ${esc(amtTxt)}</div>`
          : "";
        const keyTc =
          k.time_close_enabled && k.time_close_at_ms
            ? timeCloseSymbolBadgeHtml(k)
            : k.time_close_enabled && k.time_close_hours
              ? `<span class="pos-symbol-time-close pos-meta-on">时间平仓 ${esc(k.time_close_hours)}h</span>`
              : "";
        return `<div class="${cardCls}">
          <div class="hub-mini-title">${esc(k.symbol)} ${keyTc} · ${esc(mt)}${dir} ${pendingTag}</div>
          <div class="hub-mini-line">上沿 ${esc(k.upper)} / 下沿 ${esc(k.lower)}</div>
          ${amtLine}
          <div class="hub-mini-line hub-key-status-line">${esc(kp.gate_summary || kp.price_display || kp.price || "—")}${kp.gate_metrics ? ` · ${esc(kp.gate_metrics)}` : ""}</div>
        </div>`;
      })
      .join("");
    return `<div class="hub-key-list">${cards}</div>`;
  }

  function renderOrderMonitorSection(orders, tickMap) {
    if (!orders || !orders.length) return "";
    return orders
      .map((o) => {
        const sym = o.exchange_symbol || o.symbol || "";
        const tcBadge = o.time_close_enabled ? timeCloseSymbolBadgeHtml(o) : "";
        const fcBadge = o.force_close_enabled ? forceCloseSymbolBadgeHtml(o) : "";
        return `<div class="hub-mini-card">
        <div class="hub-mini-title">#${esc(o.id)} · ${esc(o.symbol || o.exchange_symbol)} ${tcBadge}${fcBadge} · ${renderDirectionHtml(o.direction)}</div>
        <div class="hub-mini-line">触发 ${fmtSymbolPrice(o.trigger_price, sym, tickMap)} · SL ${fmtSymbolPrice(o.stop_loss, sym, tickMap)} · TP ${fmtSymbolPrice(o.take_profit, sym, tickMap)} · ${esc(o.entry_model_label || o.trade_style || o.monitor_type || "下单监控")}</div>
      </div>`;
      })
      .join("");
  }

  function renderRollSection(rolls, tickMap) {
    if (!rolls || !rolls.length) return "";
    return rolls
      .map((g) => {
        const sym = g.symbol || g.exchange_symbol || "";
        const avg =
          g.avg_entry_display || fmtSymbolPrice(g.avg_entry, sym, tickMap) || "—";
        const tpProfit =
          g.reward_at_tp_usdt != null && g.reward_at_tp_usdt !== ""
            ? `${fmt(g.reward_at_tp_usdt, 2)}U`
            : "—";
        const legs = Array.isArray(g.recent_legs) ? g.recent_legs : [];
        const legRows = legs
          .map((leg) => {
            const legAvg =
              leg.avg_entry_display ||
              fmtSymbolPrice(leg.avg_entry_after, sym, tickMap) ||
              "—";
            const legProfit =
              leg.reward_at_tp_usdt != null && leg.reward_at_tp_usdt !== ""
                ? `${fmt(leg.reward_at_tp_usdt, 2)}U`
                : "—";
            return `<div class="hub-mini-line hub-roll-leg">腿 #${esc(leg.leg_index)} ${esc(leg.add_mode || "")} · 张 ${esc(leg.amount != null ? leg.amount : "—")} · 均价 ${legAvg} · 止盈 ${legProfit}</div>`;
          })
          .join("");
        return `<div class="hub-mini-card">
        <div class="hub-mini-title">组 #${esc(g.id)} · ${esc(g.symbol || "")} ${renderDirectionHtml(g.direction)} · 监控 #${esc(g.order_monitor_id || "—")}</div>
        <div class="hub-mini-line">腿数 ${esc(g.leg_count != null ? g.leg_count : "—")} · SL ${fmtSymbolPrice(g.current_stop_loss, sym, tickMap)} · 首仓TP ${fmtSymbolPrice(g.initial_take_profit, sym, tickMap)}</div>
        <div class="hub-mini-line">当前均价 ${avg} · 止盈盈利 ${tpProfit}</div>
        ${legRows}
      </div>`;
      })
      .join("");
  }

  function renderPositionTableRow(
    exchangeId,
    exchangeKey,
    x,
    monitorOrder,
    trendPlan,
    tickMap,
    opts
  ) {
    const options = opts || {};
    const symAttr = esc(x.symbol || "").replace(/"/g, "&quot;");
    const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, "&quot;");
    const side = sideAttr || "long";
    const contractsAttr = esc(String(x.contracts != null ? x.contracts : "")).replace(
      /"/g,
      "&quot;"
    );
    const cond = condOrdersFromPosition(x);
    const tpsl = resolvePositionTpsl(x, monitorOrder, trendPlan);
    const beSecured = isBreakevenSecured(side, tpsl.entry, monitorOrder, cond, x);
    const slAttr = esc(String(tpsl.sl)).replace(/"/g, "&quot;");
    const tpAttr = esc(String(tpsl.tp)).replace(/"/g, "&quot;");
    const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, x.symbol, x, monitorOrder, trendPlan);
    const symBeBadge = beSecured ? ` ${breakevenBadgeHtml()}` : "";
    const mo = monitorOrder || {};
    const tcBadge =
      !isTrendContext(mo, trendPlan) && mo.time_close_enabled ? timeCloseSymbolBadgeHtml(mo) : "";
    const fcBadge =
      !isTrendContext(mo, trendPlan) && mo.force_close_enabled ? forceCloseSymbolBadgeHtml(mo) : "";
    const intraday = !!options.intradayDiscipline;
    const actionCell = intraday
      ? ""
      : `<div class="pos-action-group">
              <button type="button" class="btn-place-tpsl btn-sm ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}" data-contracts="${contractsAttr}" data-sl="${slAttr}" data-tp="${tpAttr}">委托</button>
              <button type="button" class="btn-close-pos btn-sm danger" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}">平仓</button>
            </div>`;
    const pnlTd = showAccountPnlPref()
      ? `<td class="${pnlCls(x.unrealized_pnl)}">${fmt(x.unrealized_pnl, 2)}</td>`
      : "";
    const tpProfitTd = `<td>${formatTpProfitCell(monitorOrder, x)}</td>`;
    return `<tr>
          <td class="td-symbol"><button type="button" class="btn-open-market sym-link" ${mktAttrs} title="打开行情区(含入场/止盈止损)">${esc(x.symbol)}</button>${tcBadge}${fcBadge}${symBeBadge}</td>
          <td class="${sideDirCls(x.side)}">${renderDirectionHtml(x.side)}</td>
          <td class="td-entry">${fmtEntryPrice(x, tickMap)}</td>
          <td>${fmtMarkPrice(x, tickMap)}</td>
          <td>${fmt(x.contracts, 2)}</td>
          ${tpProfitTd}
          ${pnlTd}
          <td class="td-actions">${actionCell}</td>
        </tr>`;
  }

  function renderPositionBlock(exchangeId, exchangeKey, x, monitorOrder, trendPlan, tickMap, opts) {
    const options = opts || {};
    const compact = !!options.compact;
    const reg = Array.isArray(x.regular_orders) ? x.regular_orders : [];
    const cond = condOrdersFromPosition(x);
    const ordersBlock = compact
      ? ""
      : renderOrdersCollapse(exchangeId, x.symbol, cond, reg, tickMap);
    const rowHtml = renderPositionTableRow(
      exchangeId,
      exchangeKey,
      x,
      monitorOrder,
      trendPlan,
      tickMap,
      opts
    );
    return `<div class="pos-block">
      <div class="table-scroll">
      ${positionTableHeadHtml(false)}
        ${rowHtml}
      </tbody></table>
      </div>
      ${ordersBlock}
    </div>`;
  }

  const KEY_BUCKET_FIB_TYPES = new Set([
    "斐波回调0.618",
    "斐波回调0.786",
    "关键位斐波0.618",
    "关键位斐波0.786",
  ]);
  const KEY_BUCKET_BREAKOUT_TYPES = new Set([
    "箱体突破",
    "收敛突破",
    "关键位箱体突破",
    "关键位收敛突破",
    "关键位收敛结构",
  ]);
  const KEY_BUCKET_WATCH_TYPES = new Set([
    "关键支撑阻力",
    "关键阻力位",
    "关键支撑位",
    "关键位监控",
  ]);

  function classifyKeyMonitorBucket(monitorType) {
    const t = String(monitorType || "").trim();
    if (!t) return "watch";
    if (KEY_BUCKET_FIB_TYPES.has(t) || /斐波/.test(t)) return "fib";
    if (KEY_BUCKET_BREAKOUT_TYPES.has(t) || /突破/.test(t)) return "breakout";
    if (KEY_BUCKET_WATCH_TYPES.has(t) || /阻力|支撑/.test(t)) return "watch";
    return "watch";
  }

  function countKeyMonitorsByBucket(keys) {
    const counts = { breakout: 0, fib: 0, watch: 0 };
    (keys || []).forEach((k) => {
      if (!k || typeof k !== "object") return;
      const bucket = classifyKeyMonitorBucket(k.monitor_type || k.type);
      if (bucket === "breakout") counts.breakout += 1;
      else if (bucket === "fib") counts.fib += 1;
      else counts.watch += 1;
    });
    return counts;
  }

  function renderCardStrategyStats(row, hm, flaskOk, opts) {
    const options = opts || {};
    const caps = row.capabilities || [];
    const chips = [];
    if (flaskOk && hm && typeof hm === "object") {
      if (caps.includes("key") && !options.hideKeyChips) {
        const kc = countKeyMonitorsByBucket(hm.keys || []);
        if (kc.breakout > 0) chips.push({ kind: "key-breakout", label: `突破 ${kc.breakout}` });
        if (kc.fib > 0) chips.push({ kind: "key-breakout", label: `斐波 ${kc.fib}` });
        if (kc.watch > 0) chips.push({ kind: "key-watch", label: `监控 ${kc.watch}` });
      }
      if (caps.includes("trend")) {
        const trendN = Array.isArray(hm.trends) ? hm.trends.length : 0;
        if (trendN > 0) chips.push({ kind: "trend", label: `趋势回调 ${trendN}` });
      }
      const rollN = Array.isArray(hm.rolls) ? hm.rolls.length : 0;
      if (rollN > 0) chips.push({ kind: "roll", label: `顺势加仓 ${rollN}` });
    }
    // 永续独立卡不再显示「期权 N仓」(期权只看期权卡);手机磁贴仍可显示
    if (!options.hideOptions && (caps.includes("options") || rowHasOptionsLayout(row))) {
      const opt = row.options || {};
      if (opt.enabled === false) {
        chips.push({ kind: "options", label: "期权 · 未启用" });
      } else if (opt.ok === false) {
        chips.push({ kind: "options", label: "期权 · 异常" });
      } else {
        const n = Number(
          opt.position_count != null ? opt.position_count : (opt.positions || []).length
        );
        const nSafe = Number.isFinite(n) ? n : 0;
        chips.push({ kind: "options", label: nSafe > 0 ? `期权 ${nSafe}仓` : "期权" });
      }
    }
    if (!chips.length) return "";
    return `<div class="card-strategy-stats">${chips
      .map(
        (c) =>
          `<span class="card-stat-chip card-stat-${esc(c.kind)}">${esc(c.label)}</span>`
      )
      .join("")}</div>`;
  }

  function renderGridPositionsTable(exchangeId, exchangeKey, positions, orders, trends, tickMap, intradayDiscipline) {
    const intraday = !!intradayDiscipline;
    const rows = positions
      .map((p) =>
        renderPositionTableRow(
          exchangeId,
          exchangeKey,
          p,
          findMonitorOrder(orders, p.symbol, p.side),
          findTrendPlan(trends, p.symbol, p.side),
          tickMap,
          { compact: true, intradayDiscipline: intraday }
        )
      )
      .join("");
    return `<div class="pos-table-wrap table-scroll">
      ${positionTableHeadHtml(true)}
        ${rows}
      </tbody></table>
    </div>`;
  }

  function rowHasOptionsLayout(row) {
    if (!(row.capabilities || []).includes("options")) return false;
    const opt = row.options || {};
    return opt.enabled !== false;
  }

  function sumUsdtEquiv(a, b) {
    const vals = [a, b]
      .map((v) => (v == null || v === "" ? null : Number(v)))
      .filter((v) => v != null && Number.isFinite(v));
    if (!vals.length) return null;
    return vals.reduce((s, v) => s + v, 0);
  }

  function optionsBalanceFields(opt) {
    const bal = (opt && opt.balances) || opt || {};
    return {
      funding: sumUsdtEquiv(bal.funding_usdt, bal.funding_usdc),
      trading: sumUsdtEquiv(bal.trading_usdt, bal.trading_usdc),
      upl: opt && opt.upl_total_usdc != null && Number.isFinite(Number(opt.upl_total_usdc))
        ? Number(opt.upl_total_usdc)
        : null,
    };
  }

  function renderStatRow(funding, trading, upnl) {
    if (!showAccountPnlPref()) return "";
    return `<div class="stat-row">
      <div class="stat-box"><div class="stat-label">资金账户</div><div class="stat-value">${fmt(funding, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
      <div class="stat-box"><div class="stat-label">交易账户</div><div class="stat-value">${fmt(trading, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
      <div class="stat-box"><div class="stat-label">浮盈合计</div><div class="stat-value ${pnlCls(upnl)}">${fmt(upnl, 2)}</div></div>
    </div>`;
  }

  function renderAccountStatRow(row, ag) {
    return renderStatRow(row.funding_usdt, row.trading_usdt, ag.total_unrealized_pnl);
  }

  function shortOptionsInst(instId) {
    const s = String(instId || "");
    if (s.length <= 22) return s;
    return s.slice(0, 10) + "…" + s.slice(-8);
  }

  function optionsExpiryCdHtml(expMs) {
    const ms = expMs != null && expMs !== "" ? String(expMs) : "";
    if (!ms) return "—";
    return `<span class="opt-expiry-cd" data-opt-exp-ms="${esc(ms)}">—</span>`;
  }

  function findOptionsTargetForInst(targets, instId) {
    const want = String(instId || "").trim();
    if (!want) return null;
    const list = Array.isArray(targets) ? targets : [];
    return (
      list.find((t) => String((t && t.inst_id) || "").trim() === want) ||
      list.find((t) => String((t && t.instId) || "").trim() === want) ||
      null
    );
  }

  function renderOptionsTargetCell(target) {
    if (!target) return "<td>—</td>";
    const side = String(target.opt_type || "").toUpperCase() === "P" ? "Put≤" : "Call≥";
    const px = target.target_index != null ? fmt(target.target_index, 1) : "—";
    if (target.managed_by === "hedge_plan") {
      return `<td class="hub-opt-target-cell is-on is-hedge" title="由对冲计划监控">对冲#${esc(target.plan_id)} ${esc(side)} ${esc(px)}</td>`;
    }
    return `<td class="hub-opt-target-cell is-on" title="目标监控">${esc(side)} ${esc(px)}</td>`;
  }

  function renderOptionsPositionsTable(pos, targets) {
    if (!pos.length) return '<div class="empty-hint hub-slot-pos">暂无期权持仓</div>';
    let html = '<div class="table-wrap hub-options-table-wrap"><table class="hub-options-table"><thead><tr>';
    html +=
      "<th>合约</th><th>类型</th><th>张数</th><th>到期倒计时</th><th>目标监控</th><th>净盈亏</th><th>收益率</th>";
    html += "</tr></thead><tbody>";
    pos.forEach((p) => {
      const optType =
        (p.opt_type || "").toUpperCase() === "C"
          ? "Call"
          : (p.opt_type || "").toUpperCase() === "P"
            ? "Put"
            : p.opt_type || "—";
      const preview = p.close_preview || {};
      let net = preview.estimated_pnl;
      if (net == null && preview.total_received != null && p.premium_paid != null) {
        net = Number(preview.total_received) - Number(p.premium_paid);
      }
      let roi = preview.estimated_pnl_ratio_pct;
      if (roi == null && net != null && Number(p.premium_paid) > 0) {
        roi = (Number(net) / Number(p.premium_paid)) * 100;
      }
      const target = findOptionsTargetForInst(targets, p.inst_id);
      html += `<tr>
        <td><code class="hub-options-inst" title="${esc(p.inst_id || "")}">${esc(shortOptionsInst(p.inst_id))}</code></td>
        <td>${esc(optType)}</td>
        <td>${esc(p.pos)}</td>
        <td>${optionsExpiryCdHtml(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)}</td>
        ${renderOptionsTargetCell(target)}
        <td class="${pnlCls(net)}">${net == null ? "—" : fmt(net, 2)}</td>
        <td class="${pnlCls(net)}">${roi == null ? "—" : esc(Number(roi).toFixed(2)) + "%"}</td>
      </tr>`;
    });
    html += "</tbody></table></div>";
    return html;
  }

  function monitorOptionsSplitActive(rows) {
    if (isMobileLayout() || expandedExchangeId) return false;
    return (rows || []).some((r) => rowHasOptionsLayout(r));
  }

  function renderPerpetualInnerCard(row, ag, pos, orders, trends, tickMap, intraday) {
    let html = '<div class="hub-inner-card hub-inner-card-perp">';
    html += '<div class="hub-inner-card-head">永续账户</div>';
    html += '<div class="hub-inner-card-body">';
    html += renderAccountStatRow(row, ag);
    html += renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday);
    html += "</div></div>";
    return html;
  }

  function renderOptionsPositionsCards(pos) {
    if (!pos.length) return '<div class="empty-hint">暂无期权持仓</div>';
    if (!globalThis.OptionsPositionCards || !OptionsPositionCards.renderCard) {
      return renderOptionsPositionsTable(pos);
    }
    const cls = hubPosListCountClass(pos.length);
    let html = `<div class="hub-pos-list hub-opt-pos-list ${cls}" data-pos-count="${pos.length}">`;
    pos.forEach((p) => {
      html += OptionsPositionCards.renderCard(p, { readOnly: true, hub: true });
    });
    html += "</div>";
    return html;
  }

  function renderOptionsSectionBody(row, opts) {
    const options = opts || {};
    const layout = options.layout || "table";
    const opt = row.options || {};
    let html = "";
    if (opt.enabled === false) {
      html += renderStatRow(null, null, null);
      html += '<div class="section-title hub-options-title">期权持仓</div>';
      html += '<div class="empty-hint">期权未启用(OKX_OPTIONS_ENABLED)</div>';
    } else if (opt.ok === false) {
      html += renderStatRow(null, null, null);
      html += '<div class="section-title hub-options-title">期权持仓</div>';
      html += `<div class="err">${esc(opt.msg || "期权数据不可用")}</div>`;
    } else {
      const pos = Array.isArray(opt.positions) ? opt.positions : [];
      const targets = Array.isArray(opt.target_monitors) ? opt.target_monitors : [];
      const bal = optionsBalanceFields(opt);
      html += renderStatRow(bal.funding, bal.trading, bal.upl);
      html += `<div class="section-title hub-options-title">期权持仓 · ${pos.length} 仓</div>`;
      html +=
        layout === "cards"
          ? renderOptionsPositionsCards(pos)
          : renderOptionsPositionsTable(pos, targets);
    }
    return html;
  }

  function renderOptionsInnerCard(row) {
    let html = '<div class="hub-inner-card hub-inner-card-options">';
    html += '<div class="hub-inner-card-head">期权账户</div>';
    html += '<div class="hub-inner-card-body">';
    html += renderOptionsSectionBody(row);
    html += "</div></div>";
    return html;
  }

  function renderOptionsMonitorSection(row, opts) {
    if (!rowHasOptionsLayout(row)) return "";
    let html = '<div class="hub-monitor-block hub-monitor-options">';
    html += '<div class="hub-monitor-block-label">期权账户</div>';
    html += renderOptionsSectionBody(row, opts);
    html += "</div>";
    return html;
  }

  function renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday) {
    const n = Array.isArray(pos) ? pos.length : 0;
    let html = "";
    html += `<div class="section-title">交易所持仓 · ${n} 仓</div>`;
    html += `<div class="hub-slot-pos" data-pos-count="${n}">`;
    if (n) {
      html += renderGridPositionsTable(
        row.id,
        row.key || row.id,
        pos,
        orders,
        trends,
        tickMap,
        intraday
      );
    } else {
      html += '<div class="empty-hint">无持仓</div>';
    }
    html += "</div>";
    return html;
  }

  const HUB_EXPAND_HINT = "点击标题栏进入全屏 · 委托 / 关键位 / 下单监控 / 趋势回调 / 顺势加仓";

  function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap, layoutOpts) {
    const layout = layoutOpts || {};
    const tickMap = buildPriceTickMap(row);
    const intraday = isIntradayDisciplineRow(row);
    const expandHint = `<div class="card-expand-hint">${esc(HUB_EXPAND_HINT)}</div>`;
    let inner = "";
    if (layout.okxPart === "perp" || layout.splitSide) {
      /* 桌面分栏:仓位预留 + 原关键位/趋势/顺势芯片 + 底栏提示;不显示期权 N仓 */
      if (layout.okxPart === "options") {
        inner += renderOptionsSectionBody(row);
        inner += expandHint;
        return inner;
      }
      if (layout.okxPart === "perp" || !rowHasOptionsLayout(row)) {
        inner += renderAccountStatRow(row, ag);
        inner += renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday);
        inner += renderCardStrategyStats(row, hm, flaskOk, { hideOptions: true });
        if (intraday) {
          inner += `<div class="card-expand-hint">日内纪律:禁手动平仓/改委托 · 整点强制清仓${row.force_close && row.force_close.enabled ? " · " + esc(row.force_close.label || "强制清仓") : ""}</div>`;
        } else {
          inner += expandHint;
        }
        return inner;
      }
    }
    if (layout.okxPart === "options") {
      inner += renderOptionsSectionBody(row);
      inner += expandHint;
      return inner;
    }
    if (rowHasOptionsLayout(row)) {
      if (monitorGridOptionsSplit) {
        inner += '<div class="hub-inner-cards">';
        inner += renderPerpetualInnerCard(row, ag, pos, orders, trends, tickMap, intraday);
        inner += renderOptionsInnerCard(row);
        inner += "</div>";
      } else {
        inner += '<div class="hub-monitor-block hub-monitor-perp">';
        inner += '<div class="hub-monitor-block-label">永续账户</div>';
        inner += renderAccountStatRow(row, ag);
        inner += renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday);
        inner += "</div>";
        inner += renderOptionsMonitorSection(row);
      }
    } else {
      inner += renderAccountStatRow(row, ag);
      inner += renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday);
    }
    inner += renderCardStrategyStats(row, hm, flaskOk);
    inner += intraday
      ? `<div class="card-expand-hint">日内纪律:禁手动平仓/改委托 · 整点强制清仓${row.force_close && row.force_close.enabled ? " · " + esc(row.force_close.label || "强制清仓") : ""}</div>`
      : expandHint;
    return inner;
  }

  function renderFullscreenExchange(row) {
    const tickMap = buildPriceTickMap(row);
    const ag = row.agent || {};
    const pos = Array.isArray(ag.positions) ? ag.positions : [];
    const hm = row.hub_monitor || {};
    const flaskOk = row.flask_ok !== false && hm.ok !== false;
    const keys = flaskOk ? hm.keys || [] : [];
    const orders = flaskOk ? hm.orders || [] : [];
    const trends = flaskOk ? hm.trends || [] : [];
    const rolls = flaskOk ? hm.rolls || [] : [];
    const kmap = {};
    (row.key_prices || []).forEach((k) => {
      kmap[k.id] = k;
    });
    const flaskOpen = row.flask_url_browser || row.flask_url;
    const intraday = isIntradayDisciplineRow(row);
    const fcHeadBadge = intraday ? forceCloseHeadBadgeHtml(row.force_close) : "";
    let html = `<div class="fs-head">
      <div>
        <h2 class="fs-title">${esc(row.name)}${fcHeadBadge ? " " + fcHeadBadge : ""}</h2>
        <div class="fs-sub">${esc(flaskOpen || "")}</div>
      </div>
      <div class="fs-head-actions">
        <button type="button" class="ghost btn-expand-back">返回监控</button>
        ${flaskOpen ? `<a class="btn-link btn-open-instance btn-open-trade" href="#" data-ex-id="${esc(row.id)}" data-next="/trade" data-new-tab="1">打开实例</a>` : ""}
        ${flaskOpen ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/trade">下单</a>` : ""}
        ${flaskOpen ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/key_monitor">监控位</a>` : ""}
        ${flaskOpen ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/records">复盘</a>` : ""}
        ${flaskOpen && (row.capabilities || []).includes("options") ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/options">期权</a>` : ""}
        ${intraday ? "" : `<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>`}
      </div>
    </div>`;
    if (!row.http_ok || ag.ok === false) {
      html += `<div class="err">${esc(row.error || ag.error || "子代理不可用")}</div>`;
      return html;
    }
    const posCount = pos.length;
    const posListCls = hubPosListCountClass(posCount);
    if (rowHasOptionsLayout(row)) {
      html += '<div class="hub-monitor-block hub-monitor-perp">';
      html += '<div class="hub-monitor-block-label">永续账户</div>';
    }
    html += renderAccountStatRow(row, ag);
    html += `<div class="section-title">交易所持仓(${posCount} 仓 · 每币种一卡)</div>`;
    html += `<div class="hub-pos-list ${posListCls}" data-pos-count="${posCount}">`;
    if (posCount) {
      pos.forEach((p) => {
        html += renderLivePositionCard(
          row.id,
          row.key || row.id,
          p,
          findMonitorOrder(orders, p.symbol, p.side),
          findTrendPlan(trends, p.symbol, p.side),
          tickMap,
          intraday
        );
      });
    } else {
      html += '<div class="pos-empty">暂无持仓</div>';
    }
    html += "</div>";
    if (rowHasOptionsLayout(row)) {
      html += "</div>";
    }
    html += renderOptionsMonitorSection(row, { layout: "cards" });
    html += '<div class="hub-fs-sections-grid">';
    if ((row.capabilities || []).includes("key")) {
      if (!flaskOk) {
        html += renderHubSectionCard("关键位", `<div class="err">${esc(row.flask_error || hm.error || "Flask 未连通")}</div>`, "");
      } else {
        html += renderHubSectionCard(
          `关键位 · ${keys.length}`,
          renderKeySection(keys, kmap),
          "当前无关键位记录"
        );
      }
    }
    html += renderHubSectionCard("下单监控", renderOrderMonitorSection(orders, tickMap), "暂无运行中的下单监控");
    if ((row.capabilities || []).includes("trend")) {
      html += renderHubSectionCard(
        "趋势回调",
        renderTrendSection(trends, tickMap, pos, row),
        "暂无运行中的趋势回调计划"
      );
    }
    html += renderHubSectionCard("顺势加仓", renderRollSection(rolls, tickMap), "暂无运行中的顺势加仓组");
    html += "</div>";
    return html;
  }

  function openTpslModal(exchangeId, symbol, side, contracts, slHint, tpHint) {
    tpslPending = {
      exchangeId,
      symbol,
      side: (side || "long").toLowerCase(),
      contracts: parseFloat(contracts),
    };
    const modal = document.getElementById("tpsl-modal");
    const meta = document.getElementById("tpsl-modal-meta");
    const slIn = document.getElementById("tpsl-sl");
    const tpIn = document.getElementById("tpsl-tp");
    if (!modal || !meta || !slIn || !tpIn) return;
    meta.textContent = `${symbol} · ${side} · ${contracts} 张`;
    slIn.value = slHint !== "" && slHint != null ? String(slHint) : "";
    tpIn.value = tpHint !== "" && tpHint != null ? String(tpHint) : "";
    modal.classList.remove("hidden");
    modal.setAttribute("aria-hidden", "false");
    slIn.focus();
  }

  function closeTpslModal() {
    tpslPending = null;
    const modal = document.getElementById("tpsl-modal");
    if (modal) {
      modal.classList.add("hidden");
      modal.setAttribute("aria-hidden", "true");
    }
  }

  async function submitTpslModal() {
    if (!tpslPending) return;
    const slIn = document.getElementById("tpsl-sl");
    const tpIn = document.getElementById("tpsl-tp");
    const sl = parseFloat(slIn && slIn.value);
    const tp = parseFloat(tpIn && tpIn.value);
    if (!sl || sl <= 0 || !tp || tp <= 0) {
      showToast("请填写有效的止损价与止盈价", true);
      return;
    }
    const { exchangeId, symbol, side, contracts } = tpslPending;
    if (
      !confirm(
        `确认 ${symbol} ${side}\n先撤销全部条件单,再挂止损 ${sl},止盈 ${tp}?`
      )
    ) {
      return;
    }
    const btn = document.getElementById("tpsl-submit");
    if (btn) btn.disabled = true;
    try {
      const r = await apiFetch(
        "/api/orders/" + encodeURIComponent(exchangeId) + "/place-tpsl",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            symbol,
            side,
            stop_loss: sl,
            take_profit: tp,
            contracts: contracts > 0 ? contracts : null,
          }),
        }
      );
      const j = await r.json();
      const pl = j.payload || {};
      const ok = j.ok && pl.ok !== false;
      const n = pl.placed && pl.placed.cancelled_conditional;
      showToast(
        ok
          ? `已挂单(已撤 ${n != null ? n : "?"} 笔旧条件单)`
          : pl.error || JSON.stringify(j),
        !ok
      );
      if (ok) {
        closeTpslModal();
        refreshMonitorBoardNow();
      }
    } catch (e) {
      showToast(String(e), true);
    } finally {
      if (btn) btn.disabled = false;
    }
  }

  function initInstanceFrame() {
    const back = document.getElementById("instance-frame-back");
    const refresh = document.getElementById("instance-frame-refresh");
    const newTab = document.getElementById("instance-frame-newtab");
    const frame = document.getElementById("instance-frame");
    if (frame && frame.dataset.hubNavBound !== "1") {
      frame.dataset.hubNavBound = "1";
      frame.addEventListener("load", () => setInstanceFrameNavLoading(false));
    }
    if (!window.__hubInstanceFrameMsgBound) {
      window.__hubInstanceFrameMsgBound = true;
      window.addEventListener("message", (ev) => {
        const d = ev.data;
        if (!d || typeof d !== "object") return;
        if (d.type === "instance-frame-navigating") {
          if (d.embedShellTab) return;
          setInstanceFrameNavLoading(true);
        } else if (d.type === "instance-frame-ready") {
          setInstanceFrameNavLoading(false);
        }
      });
    }
    if (back) back.onclick = () => closeInstanceFrame();
    if (refresh) refresh.onclick = () => refreshInstanceFrame();
    if (newTab) {
      newTab.onclick = () => {
        if (instanceFrameCtx) {
          openInstance(instanceFrameCtx.exchangeId, instanceFrameCtx.nextPath, {
            newTab: true,
          });
          return;
        }
        if (instanceFrameUrl) window.open(instanceFrameUrl, "_blank", "noopener");
      };
    }
  }

  function initFullscreen() {
    const backdrop = document.getElementById("exchange-fullscreen-backdrop");
    if (backdrop) {
      backdrop.onclick = () => {
        closeExchangeFullscreen();
        renderMonitorGrid(lastMonitorRows);
      };
    }
    const fs = document.getElementById("exchange-fullscreen");
    if (fs && !expandedExchangeId) {
      fs.classList.add("hidden");
      fs.setAttribute("aria-hidden", "true");
    }
  }

  function initTpslModal() {
    const backdrop = document.getElementById("tpsl-modal-backdrop");
    const cancel = document.getElementById("tpsl-cancel");
    const submit = document.getElementById("tpsl-submit");
    if (backdrop) backdrop.onclick = closeTpslModal;
    if (cancel) cancel.onclick = closeTpslModal;
    if (submit) submit.onclick = () => submitTpslModal();
    document.addEventListener("keydown", (ev) => {
      if (ev.key === "Escape") {
        closeTpslModal();
        const shell = document.getElementById("instance-frame-shell");
        if (shell && !shell.classList.contains("hidden")) {
          closeInstanceFrame();
          return;
        }
        if (expandedExchangeId) {
          closeExchangeFullscreen();
          renderMonitorGrid(lastMonitorRows);
        }
      }
    });
  }

  async function cancelOneOrder(exchangeId, symbol, orderId, channel) {
    if (!confirm(`撤销委托 ${symbol} #${orderId}?`)) return;
    try {
      const r = await apiFetch("/api/orders/" + encodeURIComponent(exchangeId) + "/cancel", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ symbol, order_id: orderId, channel: channel || "regular" }),
      });
      const j = await r.json();
      const pl = j.payload || {};
      const ok = j.ok && pl.ok !== false;
      showToast(ok ? "已撤单" : pl.error || JSON.stringify(j), !ok);
      refreshMonitorBoardNow();
    } catch (e) {
      showToast(String(e), true);
    }
  }

  async function cancelSymbolOrders(exchangeId, symbol, scope) {
    const label = scope === "conditional" ? "全部条件单" : "全部委托";
    if (!confirm(`确认撤销 ${symbol} 的${label}?`)) return;
    try {
      const r = await apiFetch(
        "/api/orders/" + encodeURIComponent(exchangeId) + "/cancel-symbol",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ symbol, scope }),
        }
      );
      const j = await r.json();
      const pl = j.payload || {};
      const ok = j.ok && pl.ok !== false;
      const n = pl.cancelled_count != null ? pl.cancelled_count : "?";
      showToast(ok ? `已撤销 ${n} 笔` : pl.error || JSON.stringify(j), !ok);
      refreshMonitorBoardNow();
    } catch (e) {
      showToast(String(e), true);
    }
  }

  function renderMonitorTile(row) {
    const ag = row.agent || {};
    const pos = Array.isArray(ag.positions) ? ag.positions : [];
    const alert = analyzeExchangeAlert(row);
    const upnl = ag.total_unrealized_pnl;
    const openCount = pos.filter(positionHasContracts).length;
    const dotCls =
      alert.level === "error" ? "bad" : alert.level === "warn" ? "warn" : "ok";
    const tileCls =
      alert.level === "error"
        ? "hub-tile-error"
        : alert.level === "warn"
          ? "hub-tile-warn"
          : "hub-tile-ok";
    const ts = (lastMonitorBoardUpdatedAt || "").replace("T", " ");
    const tsShort = ts ? ts.slice(-8) : "—";
    const posLine =
      openCount > 0 ? `${openCount}仓 · ${alert.summary}` : alert.summary;
    const opt = row.options || {};
    const hasOptCap = (row.capabilities || []).includes("options") || rowHasOptionsLayout(row);
    let optLine = "";
    let pnlShow = upnl;
    let pnlSuffix = "";
    if (hasOptCap) {
      if (opt.enabled === false) {
        optLine = "期权未启用";
      } else if (opt.ok === false) {
        optLine = "期权异常";
      } else {
        const optCount = Number(
          opt.position_count != null ? opt.position_count : (opt.positions || []).length
        );
        const n = Number.isFinite(optCount) ? optCount : 0;
        const bal = typeof optionsBalanceFields === "function" ? optionsBalanceFields(opt) : {};
        const optUpl = bal && bal.upl != null ? bal.upl : null;
        optLine = n > 0 ? `期权 ${n}仓` : "期权 空仓";
        if (optUpl != null && Number.isFinite(Number(optUpl))) {
          optLine += ` · 浮盈 ${fmt(optUpl, 2)}U`;
          // 永续空仓时主数字优先展示期权浮盈,避免一直显示 0U
          if (openCount === 0) {
            pnlShow = optUpl;
            pnlSuffix = "期权";
          }
        }
      }
    }
    const hm = row.hub_monitor || {};
    const flaskOk = row.flask_ok !== false && hm.ok !== false;
    const strategyStats = renderCardStrategyStats(row, hm, flaskOk);
    return `<div class="card hub-tile ${tileCls}" data-ex-id="${esc(row.id)}">
      <div class="hub-tile-body card-expand-zone" title="点击进入全屏详情">
        <div class="hub-tile-top">
          <span class="status-dot ${dotCls}" aria-hidden="true"></span>
          <span class="hub-tile-name">${esc(row.name)}</span>
          ${formatRiskStatusBadge(hm.risk_status)}
        </div>
        ${
          showAccountPnlPref()
            ? `<div class="hub-tile-pnl ${pnlCls(pnlShow)}">${fmt(pnlShow, 2)} <small>U${
                pnlSuffix ? " · " + pnlSuffix : ""
              }</small></div>`
            : ""
        }
        <div class="hub-tile-meta">${esc(posLine)}</div>
        ${optLine ? `<div class="hub-tile-meta hub-tile-opt">${esc(optLine)}</div>` : ""}
        ${strategyStats}
        <div class="hub-tile-foot">UPD ${esc(tsShort)}</div>
      </div>
    </div>`;
  }

  function renderMonitorCard(row, layoutOpts) {
    const opts = layoutOpts || {};
    const ag = row.agent || {};
    const pos = Array.isArray(ag.positions) ? ag.positions : [];
    const hm = row.hub_monitor || {};
    const flaskOk = row.flask_ok !== false && hm.ok !== false;
    const keys = flaskOk ? hm.keys || [] : [];
    const orders = flaskOk ? hm.orders || [] : [];
    const trends = flaskOk ? hm.trends || [] : [];
    const rolls = flaskOk ? hm.rolls || [] : [];
    const kmap = {};
    (row.key_prices || []).forEach((k) => {
      kmap[k.id] = k;
    });
    let inner = "";
    const agOk = ag.ok !== false;
    const agErr = ag.error || row.error || "";
    if (!row.http_ok) {
      inner = `<div class="err">${esc(row.error || "子代理不可用")}</div>`;
    } else if (!agOk) {
      inner = `<div class="err">${esc(agErr || "子代理返回失败")}</div>`;
      inner += `<div class="empty-hint">请检查 PM2 子代理与 <code>${esc(row.agent_url || "")}/status</code></div>`;
    } else {
      inner = renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap, opts);
    }
    const online = row.http_ok && agOk;
    const cardCls = online ? "card-online" : "card-offline";
    const dotCls = online ? "ok" : "bad";
    const flaskOpen = row.flask_url_browser || row.flask_url;
    const okxPart = opts.okxPart || "";
    const isOkxOptionsCard = okxPart === "options";
    const isOkxPerpCard = okxPart === "perp";
    const titleSuffix = isOkxPerpCard ? " · 永续" : isOkxOptionsCard ? " · 期权" : "";
    const openFlask = flaskOpen
      ? `<a class="btn-link btn-open-instance btn-open-trade" href="#" data-ex-id="${esc(row.id)}" data-next="/trade" data-new-tab="1">打开实例</a>`
      : "";
    const openTrade =
      flaskOpen && !isOkxOptionsCard
        ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/trade">下单</a>`
        : "";
    const openKey =
      flaskOpen && !isOkxOptionsCard
        ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/key_monitor">监控位</a>`
        : "";
    const openReview = flaskOpen
      ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/records">复盘</a>`
      : "";
    const openOptions =
      flaskOpen && ((row.capabilities || []).includes("options") || isOkxOptionsCard)
        ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/options">期权</a>`
        : "";
    const intraday = isIntradayDisciplineRow(row);
    const fcHeadBadge = intraday && !isOkxOptionsCard ? forceCloseHeadBadgeHtml(row.force_close) : "";
    const showCloseAll = !intraday && !isOkxOptionsCard;
    const layoutCls = opts.okxSplit
      ? " card-monitor-okx-split"
      : opts.splitSide || okxPart
        ? " card-monitor-split-side"
        : "";
    const partAttr = okxPart ? ` data-okx-part="${esc(okxPart)}"` : "";
    return `<div class="card ${cardCls}${layoutCls}" data-ex-id="${esc(row.id)}"${partAttr}>
      <div class="card-head card-expand-zone" title="点击放大全屏">
        <div>
          <div class="card-title-row">
            <span class="status-dot ${dotCls}" title="${online ? "在线" : "离线"}"></span>
            <div class="card-title"><span>${esc(row.name)}${esc(titleSuffix)}</span>${fcHeadBadge}${formatRiskStatusBadge(hm.risk_status)}</div>
          </div>
          <div class="card-sub">${esc(flaskOpen || "")}</div>
        </div>
        <div class="card-actions">
          ${openFlask}
          ${openTrade}
          ${openKey}
          ${openOptions}
          ${openReview}
          ${showCloseAll ? `<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>` : ""}
        </div>
      </div>
      <div class="card-body">${inner}</div>
    </div>`;
  }

  async function hubTrendPlanStop(exchangeId, planId) {
    if (!exchangeId || !planId) {
      showToast("缺少交易所或计划 ID", true);
      return;
    }
    if (!confirm("结束计划:市价平仓并撤掉该合约全部挂单,确定?")) return;
    try {
      const r = await apiFetch("/api/trend/" + encodeURIComponent(exchangeId) + "/stop", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ plan_id: Number(planId) }),
      });
      const j = await r.json();
      showToast(j.message || (j.ok ? "已结束趋势回调计划" : "结束失败"), !j.ok);
      if (j.ok) refreshMonitorBoardNow();
    } catch (e) {
      showToast(String(e), true);
    }
  }

  async function hubTrendPlanBreakeven(exchangeId, planId, inputEl) {
    if (!exchangeId || !planId) {
      showToast("缺少交易所或计划 ID", true);
      return;
    }
    const raw = inputEl ? String(inputEl.value || "").trim() : "";
    let pct = null;
    if (raw !== "") {
      pct = Number(raw);
      if (!Number.isFinite(pct) || pct < 0) {
        showToast("保本偏移% 须为非负数", true);
        return;
      }
    }
    if (
      !confirm(
        "确认保本?将结束本趋势计划,持仓移交「下单监控」,并在交易所挂保本止损与计划止盈;后续平仓写入交易记录."
      )
    ) {
      return;
    }
    try {
      const body = { plan_id: Number(planId) };
      if (pct != null) body.breakeven_offset_pct = pct;
      const r = await apiFetch("/api/trend/" + encodeURIComponent(exchangeId) + "/breakeven", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const j = await r.json();
      showToast(j.message || (j.ok ? "保本移交成功" : "保本移交失败"), !j.ok);
      if (j.ok) refreshMonitorBoardNow();
    } catch (e) {
      showToast(String(e), true);
    }
  }

  async function closeOnePosition(exchangeId, symbol, side) {
    const label = `${symbol} · ${side}`;
    if (!confirm(`确认对该账户市价平仓:${label}?`)) return;
    try {
      const r = await apiFetch(
        "/api/close/" + encodeURIComponent(exchangeId) + "/position",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ symbol, side }),
        }
      );
      const j = await r.json();
      const pl = j.payload || {};
      const ok = j.ok && pl.ok !== false;
      const msg =
        (ok && pl.closed
          ? `已平仓 ${pl.closed.symbol} ${pl.closed.side} · 张数 ${pl.closed.amount}`
          : pl.error) || JSON.stringify(j, null, 2);
      showToast(msg, !ok);
      refreshMonitorBoardNow();
    } catch (e) {
      showToast(String(e), true);
    }
  }

  async function closeOne(id) {
    if (!confirm("确认对该账户市价全平?")) return;
    try {
      const r = await apiFetch("/api/close/" + encodeURIComponent(id), { method: "POST" });
      const j = await r.json();
      showToast(JSON.stringify(j, null, 2), !r.ok);
      refreshMonitorBoardNow();
    } catch (e) {
      showToast(String(e), true);
    }
  }

  async function closeAll() {
    const n = enabledAccounts().length;
    if (!confirm(`对 ${n} 个已启用账户执行紧急全平?`)) return;
    try {
      const r = await apiFetch("/api/close-all", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ exclude_ids: [] }),
      });
      const j = await r.json();
      showToast(JSON.stringify(j, null, 2), !r.ok);
      refreshMonitorBoardNow();
    } catch (e) {
      showToast(String(e), true);
    }
  }

  async function loadSettingsMetaLine() {
    try {
      const r = await apiFetch("/api/settings/meta");
      const m = await r.json();
      const el = document.getElementById("settings-meta-line");
      if (!el) return;
      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);
      else parts.push("未设 HUB_PUBLIC_ORIGIN(复盘链接仅本机可开)");
      if ((m.env_disabled_ids || []).length) {
        parts.push("环境强制关闭 id: " + m.env_disabled_ids.join(", ") + "(改 .env 后须重启 hub)");
      } else {
        parts.push("HUB_DISABLED_IDS 未强制关闭任何账户");
      }
      el.textContent = parts.join(" · ");
    } catch (_) {}
  }

  function renderSettingsList(data) {
    const list = document.getElementById("settings-list");
    if (!list) return;
    list.innerHTML = (data.exchanges || [])
      .map((ex, idx) => renderSettingsCard(ex, idx))
      .join("");
    list.querySelectorAll(".btn-del-ex").forEach((btn) => {
      btn.onclick = () => {
        const i = Number(btn.dataset.idx);
        data.exchanges.splice(i, 1);
        settingsCache = data;
        renderSettingsList(data);
      };
    });
    bindSettingsCardFolds(list);
    list.querySelectorAll(".settings-card-save").forEach((btn) => {
      btn.addEventListener("click", () => {
        void saveSettingsSection("exchange", { label: btn.dataset.label || "账户" });
      });
    });
  }

  const SETTINGS_FOLD_KEY = "hub_settings_section_fold";

  function settingsFoldStorageKey(section, cardKey) {
    return cardKey ? `${SETTINGS_FOLD_KEY}_${section}_${cardKey}` : `${SETTINGS_FOLD_KEY}_${section}`;
  }

  function getSettingsFoldState(section, cardKey) {
    try {
      return localStorage.getItem(settingsFoldStorageKey(section, cardKey)) === "1";
    } catch (_) {
      return false;
    }
  }

  function setSettingsFoldState(section, collapsed, cardKey) {
    try {
      localStorage.setItem(settingsFoldStorageKey(section, cardKey), collapsed ? "1" : "0");
    } catch (_) {}
  }

  function applySettingsSectionFold(el) {
    const section = el.dataset.settingsSection;
    if (!section) return;
    const collapsed = getSettingsFoldState(section);
    el.classList.toggle("is-collapsed", collapsed);
    const btn = el.querySelector(":scope > .settings-section-head > .settings-section-fold");
    if (btn) btn.setAttribute("aria-expanded", collapsed ? "false" : "true");
  }

  function applySettingsCardFold(card) {
    const key = card.dataset.key || card.dataset.idx || "";
    const collapsed = getSettingsFoldState("exchange", String(key));
    card.classList.toggle("is-collapsed", collapsed);
    const btn = card.querySelector(".settings-card-fold");
    if (btn) btn.setAttribute("aria-expanded", collapsed ? "false" : "true");
  }

  function bindSettingsCardFolds(root) {
    (root || document).querySelectorAll(".settings-card").forEach((card) => {
      if (card.dataset.foldBound === "1") return;
      card.dataset.foldBound = "1";
      applySettingsCardFold(card);
      const foldBtn = card.querySelector(".settings-card-fold");
      if (!foldBtn) return;
      foldBtn.addEventListener("click", () => {
        const key = card.dataset.key || card.dataset.idx || "";
        const collapsed = !card.classList.contains("is-collapsed");
        card.classList.toggle("is-collapsed", collapsed);
        foldBtn.setAttribute("aria-expanded", collapsed ? "false" : "true");
        setSettingsFoldState("exchange", collapsed, String(key));
      });
    });
  }

  function initSettingsSectionFolds() {
    document.querySelectorAll(".settings-section-save").forEach((btn) => {
      if (btn.dataset.saveBound === "1") return;
      btn.dataset.saveBound = "1";
      btn.addEventListener("click", () => {
        const section = btn.dataset.settingsSection || "";
        if (section === "macro") {
          const form = document.getElementById("macro-event-form");
          if (form) form.requestSubmit();
          return;
        }
        const label =
          section === "display"
            ? "显示与导航"
            : section === "supervisor"
              ? "交易监管"
              : 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 renderHubAiSyncStatus(sync) {
    const el = document.getElementById("hub-ai-sync-status");
    if (!el) return;
    if (!sync) {
      el.textContent = "";
      return;
    }
    if (sync.all_synced) {
      el.textContent = "三所实例 AI 配置已与中控同步";
      el.classList.remove("is-err");
      return;
    }
    const parts = [];
    const inst = sync.instances || {};
    Object.keys(inst).forEach((ex) => {
      const row = inst[ex];
      if (row && !row.ok) parts.push(`${ex} 不一致`);
    });
    el.textContent = parts.length ? `未完全同步:${parts.join(",")}` : "同步状态未知";
    el.classList.add("is-err");
  }

  function renderHubAiEnvFields(fields) {
    const box = document.getElementById("hub-ai-env-fields");
    if (!box) return;
    box.innerHTML = (fields || [])
      .map((f) => {
        const wide = f.key === "OPENAI_API_BASE" || f.key === "OLLAMA_API" ? " field-wide" : "";
        const inputType = f.sensitive ? "password" : f.type === "int" ? "number" : "text";
        const placeholder = f.sensitive && f.has_value ? f.masked || "****" : f.note || "";
        const value = f.sensitive ? "" : esc(f.current || f.default || "");
        const note = f.note ? `<span class="field-note">${esc(f.note)}</span>` : "";
        return `<div class="field${wide}">
          <label for="hub-ai-${esc(f.key)}">${esc(f.label || f.key)}${note}</label>
          <input id="hub-ai-${esc(f.key)}" class="hub-ai-field" data-ai-key="${esc(f.key)}" type="${inputType}" value="${value}" placeholder="${esc(placeholder)}" autocomplete="off" />
        </div>`;
      })
      .join("");
  }

  async function loadHubAiEnvSettings() {
    const box = document.getElementById("hub-ai-env-fields");
    if (!box) return;
    try {
      const r = await apiFetch("/api/settings/ai-env");
      const j = await r.json();
      if (!r.ok) throw new Error(j.detail || "加载失败");
      renderHubAiEnvFields(j.fields || []);
      renderHubAiSyncStatus(j.sync_status);
    } catch (e) {
      box.innerHTML = `<span class="err">${esc(String(e.message || e))}</span>`;
    }
  }

  function collectHubAiEnvValues() {
    const values = {};
    document.querySelectorAll(".hub-ai-field[data-ai-key]").forEach((el) => {
      values[el.dataset.aiKey] = el.value;
    });
    return values;
  }

  async function parseApiJson(r) {
    const ct = (r.headers.get("content-type") || "").toLowerCase();
    if (!ct.includes("application/json")) {
      const text = await r.text();
      const snippet = (text || "").replace(/\s+/g, " ").trim().slice(0, 120);
      throw new Error(snippet ? `服务返回非 JSON:${snippet}` : `HTTP ${r.status}`);
    }
    return r.json();
  }

  async function saveHubAiEnv() {
    const status = document.getElementById("hub-ai-env-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/ai-env", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ values: collectHubAiEnvValues(), restart: true }),
      });
      const j = await parseApiJson(r);
      if (!r.ok) throw new Error(j.detail || j.msg || "保存失败");
      if (!j.changed || !Object.keys(j.changed).length) {
        setStatus("未修改(与当前配置相同)");
        showToast("未修改");
        return;
      }
      if (j.restart_required) {
        setStatus("已同步三所,服务重启中…");
        await waitHubHealth();
      }
      setStatus("AI 配置已保存并同步至三所实例");
      showToast("AI 配置已保存并同步");
      await loadHubAiEnvSettings();
    } catch (e) {
      setStatus(String(e.message || e), true);
      showToast(String(e.message || e), true);
    }
  }

  function initHubAiEnvSettings() {
    const btn = document.getElementById("hub-ai-env-save-btn");
    if (btn && btn.dataset.bound !== "1") {
      btn.dataset.bound = "1";
      btn.addEventListener("click", () => {
        void saveHubAiEnv();
      });
    }
    void loadHubAiEnvSettings();
  }

  function macroDatetimeLocalToApi(v) {
    if (!v) return "";
    return String(v).trim().replace("T", " ").slice(0, 16);
  }

  function macroApiToDatetimeLocal(s) {
    if (!s) return "";
    return String(s).trim().replace(" ", "T").slice(0, 16);
  }

  function resetMacroEventForm() {
    macroCalendarEditId = null;
    const form = document.getElementById("macro-event-form");
    const cancel = document.getElementById("macro-event-cancel");
    const submit = document.getElementById("macro-event-submit");
    if (form) form.reset();
    if (cancel) cancel.classList.add("hidden");
    if (submit) submit.textContent = "添加";
  }

  function renderMacroEventList(events) {
    const box = document.getElementById("macro-event-list");
    if (!box) return;
    const rows = events || [];
    if (!rows.length) {
      box.innerHTML = '<div class="macro-event-empty">暂无已录入的关键数据.请在上方添加 FOMC / CPI / 就业发布时间.</div>';
      return;
    }
    const now = Date.now();
    box.innerHTML = rows
      .map((ev) => {
        const start = Number(ev.event_at_ms) - 3600000;
        const end = Number(ev.event_at_ms) + 3600000;
        const active = now >= start && now <= end;
        const note = ev.note ? `<div class="macro-event-row-meta">${esc(ev.note)}</div>` : "";
        return `<div class="macro-event-row${active ? " is-active" : ""}" data-id="${ev.id}">
          <div>
            <div class="macro-event-row-title">${esc(ev.event_type_label || ev.event_type)}</div>
            ${note}
          </div>
          <div class="macro-event-row-meta">${esc(ev.event_at || "")}</div>
          <div class="macro-event-row-meta">${active ? "窗口内" : "待触发"} · ±1h</div>
          <div class="macro-event-row-actions">
            <button type="button" class="ghost macro-event-edit" data-id="${ev.id}">编辑</button>
            <button type="button" class="ghost danger macro-event-del" data-id="${ev.id}">删除</button>
          </div>
        </div>`;
      })
      .join("");
    box.querySelectorAll(".macro-event-edit").forEach((btn) => {
      btn.addEventListener("click", () => {
        const id = Number(btn.getAttribute("data-id"));
        const row = rows.find((x) => Number(x.id) === id);
        if (!row) return;
        macroCalendarEditId = id;
        const typeEl = document.getElementById("macro-event-type");
        const atEl = document.getElementById("macro-event-at");
        const noteEl = document.getElementById("macro-event-note");
        const cancel = document.getElementById("macro-event-cancel");
        const submit = document.getElementById("macro-event-submit");
        if (typeEl) typeEl.value = row.event_type || "fomc";
        if (atEl) atEl.value = macroApiToDatetimeLocal(row.event_at || "");
        if (noteEl) noteEl.value = row.note || "";
        if (cancel) cancel.classList.remove("hidden");
        if (submit) submit.textContent = "保存";
      });
    });
    box.querySelectorAll(".macro-event-del").forEach((btn) => {
      btn.addEventListener("click", async () => {
        const id = btn.getAttribute("data-id");
        if (!id || !confirm("确定删除这条宏观关键数据?")) return;
        try {
          const r = await apiFetch(`/api/macro-calendar/events/${id}`, { method: "DELETE" });
          const j = await r.json();
          if (!j.ok) throw new Error(j.detail || "删除失败");
          showToast("已删除");
          resetMacroEventForm();
          await loadMacroCalendarUI();
          void refreshMacroRiskBanner(lastMonitorRows);
        } catch (e) {
          showToast(String(e), true);
        }
      });
    });
  }

  async function loadMacroCalendarUI() {
    const box = document.getElementById("macro-event-list");
    if (!box) return;
    try {
      const r = await apiFetch("/api/macro-calendar/events");
      const j = await r.json();
      renderMacroEventList((j.ok && j.events) || []);
    } catch (e) {
      box.innerHTML = `<div class="macro-event-empty">${esc(String(e))}</div>`;
    }
  }

  function initMacroCalendarSettings() {
    const form = document.getElementById("macro-event-form");
    const cancel = document.getElementById("macro-event-cancel");
    if (cancel) {
      cancel.addEventListener("click", () => resetMacroEventForm());
    }
    if (!form || form.dataset.bound === "1") return;
    form.dataset.bound = "1";
    form.addEventListener("submit", async (ev) => {
      ev.preventDefault();
      const typeEl = document.getElementById("macro-event-type");
      const atEl = document.getElementById("macro-event-at");
      const noteEl = document.getElementById("macro-event-note");
      const payload = {
        event_type: typeEl ? typeEl.value : "",
        event_at: macroDatetimeLocalToApi(atEl ? atEl.value : ""),
        note: noteEl ? noteEl.value : "",
      };
      try {
        const editing = macroCalendarEditId != null;
        const r = await apiFetch(
          editing
            ? `/api/macro-calendar/events/${macroCalendarEditId}`
            : "/api/macro-calendar/events",
          {
            method: editing ? "PATCH" : "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(payload),
          }
        );
        const j = await r.json();
        if (!r.ok || !j.ok) throw new Error(j.detail || "保存失败");
        showToast(editing ? "已更新" : "已添加");
        resetMacroEventForm();
        await loadMacroCalendarUI();
        void refreshMacroRiskBanner(lastMonitorRows);
      } catch (e) {
        showToast(String(e), true);
      }
    });
  }

  function loadSettingsUI() {
    loadSettingsMetaLine();
    initMacroCalendarSettings();
    initHubPasswordSettings();
    initHubAiEnvSettings();
    loadMacroCalendarUI();
    loadSettings().then((data) => {
      syncDisplayPrefsUI(data);
      syncSupervisorSettingsUI(data);
      renderSettingsList(data);
      initSettingsSectionFolds();
      if (typeof initBackupSettingsUI === "function") void initBackupSettingsUI();
    });
  }

  function renderSettingsCard(ex, idx) {
    const caps = ex.capabilities || [];
    const envOff = ex.env_disabled
      ? '<span class="badge">环境变量强制关</span>'
      : "";
    const cardKey = esc(ex.key || ex.id || String(idx));
    const cardTitle = esc(ex.name || ex.key || `账户 ${idx + 1}`);
    return `<div class="settings-card" data-idx="${idx}" data-key="${cardKey}">
      <div class="settings-card-topbar">
        <button type="button" class="settings-card-fold" aria-expanded="true" aria-label="折叠"></button>
        <span class="settings-card-title">${cardTitle}</span>
        <button type="button" class="primary settings-card-save" data-label="${cardTitle}">保存</button>
      </div>
      <div class="settings-card-body">
      <div class="settings-card-head">
        <label class="chk-label"><input type="checkbox" class="ex-enabled" ${ex.enabled ? "checked" : ""} ${ex.env_disabled ? "disabled" : ""}/> 启用</label>
        ${envOff}
        <input class="ex-name" value="${esc(ex.name || "")}" placeholder="显示名称" />
      </div>
      <div class="settings-grid">
        <div class="field"><label>Flask URL</label><input class="ex-flask" value="${esc(ex.flask_url || "")}" /></div>
        <div class="field"><label>Agent URL</label><input class="ex-agent" value="${esc(ex.agent_url || "")}" /></div>
        <div class="field field-wide"><label>复盘链接(可空)</label><input class="ex-review" value="${esc(ex.review_url || "")}" placeholder="留空则自动生成 /records" /></div>
      </div>
      <div class="cap-chips">
        <label><input type="checkbox" class="cap-key" ${caps.includes("key") ? "checked" : ""}/> 监控关键位</label>
        <label><input type="checkbox" class="cap-trend" ${caps.includes("trend") ? "checked" : ""}/> 监控趋势计划</label>
        <label><input type="checkbox" class="cap-options" ${caps.includes("options") ? "checked" : ""}/> 监控期权</label>
      </div>
      <div class="settings-card-foot">
        <div class="field"><label>id</label><input class="ex-id" value="${esc(ex.id || "")}" /></div>
        <button type="button" class="danger btn-del-ex" data-idx="${idx}">删除账户</button>
      </div>
      </div>
    </div>`;
  }

  function collectSettingsFromUI() {
    const rows = [...document.querySelectorAll("#settings-list .settings-card")];
    const pnlCb = document.getElementById("pref-show-account-pnl");
    const fundsCb = document.getElementById("pref-show-nav-funds");
    const dashCb = document.getElementById("pref-show-nav-dashboard");
    const aiCb = document.getElementById("pref-show-nav-ai");
    const calcCb = document.getElementById("pref-show-nav-calculator");
    const supEnabled = document.getElementById("supervisor-enabled");
    const supProg = document.getElementById("supervisor-wechat-program");
    const supWebhook = document.getElementById("supervisor-wechat-webhook");
    const supLink = document.getElementById("supervisor-wechat-link");
    const supPrefix = document.getElementById("supervisor-wechat-prefix");
    const supDaily = document.getElementById("supervisor-daily-warn");
    const supInterval = document.getElementById("supervisor-interval-warn");
    const supFreq30 = document.getElementById("supervisor-freq-30m");
    const supReopen = document.getElementById("supervisor-reopen-min");
    return {
      version: 1,
      display: {
        show_account_pnl: pnlCb ? !!pnlCb.checked : true,
        show_nav_funds: fundsCb ? !!fundsCb.checked : true,
        show_nav_dashboard: dashCb ? !!dashCb.checked : true,
        show_nav_plan: false,
        show_nav_archive: false,
        show_nav_quotes: false,
        show_nav_ai: aiCb ? !!aiCb.checked : true,
        show_nav_calculator: calcCb ? !!calcCb.checked : true,
        show_nav_strategy: false,
        show_nav_help: false,
        show_nav_logs: false,
      },
      supervisor: {
        enabled: supEnabled ? !!supEnabled.checked : true,
        wechat_webhook: supWebhook ? supWebhook.value.trim() : "",
        wechat_link_base: supLink ? supLink.value.trim() : "",
        wechat_prefix: supPrefix ? supPrefix.value.trim() : "【交易监管】",
        wechat_on_program_tp_sl: supProg ? !!supProg.checked : true,
        manual_close_daily_warn: supDaily ? Number(supDaily.value) || 2 : 2,
        interval_warn_minutes: supInterval ? Number(supInterval.value) || 15 : 15,
        freq_30m_count: supFreq30 ? Number(supFreq30.value) || 2 : 2,
        reopen_after_close_minutes: supReopen ? Number(supReopen.value) || 30 : 30,
      },
      exchanges: rows.map((card) => {
        const caps = [];
        if (card.querySelector(".cap-key").checked) caps.push("key");
        if (card.querySelector(".cap-trend").checked) caps.push("trend");
        if (card.querySelector(".cap-options") && card.querySelector(".cap-options").checked) caps.push("options");
        const id = card.querySelector(".ex-id").value.trim();
        const stableKey = (card.dataset.key || id).trim();
        return {
          id: id,
          key: stableKey,
          name: card.querySelector(".ex-name").value.trim(),
          flask_url: card.querySelector(".ex-flask").value.trim(),
          agent_url: card.querySelector(".ex-agent").value.trim(),
          review_url: card.querySelector(".ex-review").value.trim(),
          enabled: card.querySelector(".ex-enabled").checked,
          capabilities: caps,
        };
      }),
    };
  }

  async function saveSettingsSection(section, opts) {
    const options = opts || {};
    const body = collectSettingsFromUI();
    try {
      const r = await apiFetch("/api/settings", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const j = await r.json();
      if (!j.ok) {
        showToast("保存失败", true);
        return;
      }
      const label = options.label || "设置";
      showToast(`${label}已保存`);
      if (j.settings) {
        settingsCache = j.settings;
        syncDisplayPrefsUI(j.settings);
        syncSupervisorSettingsUI(j.settings);
        renderSettingsList(j.settings);
        loadSettingsMetaLine();
      }
      if (lastMonitorRows.length) renderMonitorGrid(lastMonitorRows);
      if (!pageNavAllowed(currentPage())) {
        history.replaceState({}, "", "/monitor");
        setActiveNav();
      }
    } catch (e) {
      showToast(String(e), true);
    }
  }

  document.getElementById("btn-logout").onclick = async () => {
    try {
      await fetch("/api/auth/logout", { method: "POST" });
    } catch (_) {}
    location.href = "/login";
  };

  document.getElementById("btn-monitor-refresh").onclick = () => refreshMonitorBoardNow();
  document.getElementById("auto-monitor").onchange = () => {
    if (document.getElementById("auto-monitor").checked) {
      connectMonitorBoardStream();
    } else {
      closeMonitorBoardStream();
    }
  };
  document.getElementById("btn-close-all").onclick = closeAll;
  document.getElementById("btn-settings-add").onclick = () => {
    const data = settingsCache || { exchanges: [] };
    const nid = String(Date.now() % 100000);
    data.exchanges.push({
      id: nid,
      key: "custom_" + nid,
      name: "新交易所",
      flask_url: "http://127.0.0.1:5000",
      agent_url: "http://127.0.0.1:15200",
      review_url: "",
      enabled: false,
      capabilities: ["key"],
    });
    settingsCache = data;
    renderSettingsList(data);
    showToast("已添加一行,请填写 URL 后点「保存设置」");
  };

  let aiChatLoading = false;
  let aiChatSessionCache = null;
  let aiChatSessionsCache = [];
  let aiSelectedBotMode = "trading";
  const AI_CHAT_MAX_ATTACHMENTS = 3;
  let aiChatPendingFiles = [];
  const aiChatMdCache = new Map();
  const AI_CHAT_MD_CACHE_MAX = 120;

  function aiChatFileKind(file) {
    return file && file.type && file.type.startsWith("image/") ? "image" : "text";
  }

  function isValidAiChatFile(file) {
    if (!file) return false;
    if (file.type && file.type.startsWith("image/")) return true;
    const mime = (file.type || "").toLowerCase();
    if (["text/plain", "text/markdown", "application/json"].includes(mime)) return true;
    const name = (file.name || "").toLowerCase();
    return (
      name.endsWith(".txt") ||
      name.endsWith(".md") ||
      name.endsWith(".markdown") ||
      name.endsWith(".json")
    );
  }

  function syncAiChatFileInput() {
    const fileInput = document.getElementById("ai-chat-files");
    if (!fileInput || typeof DataTransfer === "undefined") return;
    const dt = new DataTransfer();
    aiChatPendingFiles.forEach((f) => dt.items.add(f));
    fileInput.files = dt.files;
  }

  function renderAiChatPendingAttachments() {
    const box = document.getElementById("ai-chat-pending");
    if (!box) return;
    if (!aiChatPendingFiles.length) {
      box.innerHTML = "";
      box.hidden = true;
      return;
    }
    box.hidden = false;
    box.innerHTML = aiChatPendingFiles
      .map((f, idx) => {
        const kind = aiChatFileKind(f);
        const icon = kind === "image" ? "图" : "文";
        return (
          `<span class="ai-chat-pending-chip" data-pending-idx="${idx}">` +
          `<span class="ai-chat-pending-kind">${icon}</span>` +
          `<span class="ai-chat-pending-name" title="${esc(f.name || "附件")}">${esc(f.name || "附件")}</span>` +
          `<button type="button" class="ai-chat-pending-del" data-pending-del="${idx}" title="移除" aria-label="移除附件">×</button>` +
          `</span>`
        );
      })
      .join("");
  }

  function addAiChatPendingFiles(files) {
    const incoming = Array.isArray(files) ? files : [];
    if (!incoming.length) return;
    let added = 0;
    for (const file of incoming) {
      if (aiChatPendingFiles.length >= AI_CHAT_MAX_ATTACHMENTS) {
        showToast(`最多 ${AI_CHAT_MAX_ATTACHMENTS} 个附件`, true);
        break;
      }
      if (!isValidAiChatFile(file)) {
        showToast(`${file.name || "文件"}: 不支持的类型(仅图片或 txt/md/json)`, true);
        continue;
      }
      aiChatPendingFiles.push(file);
      added += 1;
    }
    if (!added) return;
    syncAiChatFileInput();
    renderAiChatPendingAttachments();
  }

  function removeAiChatPendingFile(index) {
    if (index < 0 || index >= aiChatPendingFiles.length) return;
    aiChatPendingFiles.splice(index, 1);
    syncAiChatFileInput();
    renderAiChatPendingAttachments();
  }

  function clearAiChatPendingFiles() {
    aiChatPendingFiles = [];
    syncAiChatFileInput();
    renderAiChatPendingAttachments();
  }

  function handleAiChatPaste(ev) {
    if (aiChatLoading) return;
    const clipboard = ev.clipboardData;
    if (!clipboard || !clipboard.items) return;
    const imageFiles = [];
    for (const item of clipboard.items) {
      if (!item.type || !item.type.startsWith("image/")) continue;
      const blob = item.getAsFile();
      if (!blob) continue;
      const sub = (item.type.split("/")[1] || "png").toLowerCase();
      const ext = sub === "jpeg" ? "jpg" : sub;
      const name = `screenshot-${Date.now()}.${ext}`;
      imageFiles.push(new File([blob], name, { type: item.type }));
    }
    if (!imageFiles.length) return;
    ev.preventDefault();
    addAiChatPendingFiles(imageFiles);
  }

  function renderHubMarkdown(text, cacheKey) {
    const raw = String(text || "");
    if (cacheKey && aiChatMdCache.has(cacheKey)) {
      return aiChatMdCache.get(cacheKey);
    }
    let html;
    if (typeof window !== "undefined" && window.AiReviewRender && window.AiReviewRender.renderMarkdown) {
      html = window.AiReviewRender.renderMarkdown(raw);
    } else {
      html = esc(raw)
        .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
        .replace(/\n/g, "<br>");
    }
    if (cacheKey) {
      if (aiChatMdCache.size >= AI_CHAT_MD_CACHE_MAX) {
        const firstKey = aiChatMdCache.keys().next().value;
        if (firstKey != null) aiChatMdCache.delete(firstKey);
      }
      aiChatMdCache.set(cacheKey, html);
    }
    return html;
  }

  function scrollAiChatToEnd() {
    const box = document.getElementById("ai-chat-messages");
    if (!box) return;
    const run = () => {
      box.scrollTop = box.scrollHeight;
      const rows = box.querySelectorAll(".ai-msg-row");
      const last = rows[rows.length - 1];
      if (last && last.scrollIntoView) {
        try {
          last.scrollIntoView({ block: "end", behavior: "auto" });
        } catch (_) {
          /* ignore */
        }
      }
    };
    requestAnimationFrame(() => requestAnimationFrame(run));
  }

  function updateAiBotTabs(mode) {
    const m = normalizeAiBotMode(mode);
    aiSelectedBotMode = m;
    document.querySelectorAll(".ai-bot-tab").forEach((btn) => {
      const on = normalizeAiBotMode(btn.dataset.bot || "trading") === m;
      btn.classList.toggle("is-active", on);
      btn.setAttribute("aria-selected", on ? "true" : "false");
    });
    const newBtn = document.getElementById("btn-ai-chat-new");
    if (newBtn) newBtn.classList.toggle("hidden", m === "supervisor");
    const histPanel = document.querySelector(".ai-chat-history-panel");
    if (histPanel) histPanel.classList.toggle("hidden", m === "supervisor");
    const input = document.getElementById("ai-chat-input");
    if (input) {
      if (m === "general") {
        input.placeholder = "随便聊点什么,不绑交易数据…可直接 Ctrl+V 粘贴截图";
      } else if (m === "supervisor") {
        input.placeholder = "回应监管提醒,说说为什么又开了一单…";
      } else {
        input.placeholder = "聊聊行情,心态,纪律,执行…;可直接 Ctrl+V 粘贴截图";
      }
    }
  }

  function renderAiChatHistory(sessions) {
    const list = document.getElementById("ai-chat-history-list");
    if (!list) return;
    const items = Array.isArray(sessions) ? sessions : [];
    if (!items.length) {
      list.innerHTML = '<p class="ai-placeholder">暂无历史,发送消息后会出现在这里.</p>';
      return;
    }
    list.innerHTML = items
      .map((s) => {
        const mode = s.bot_mode === "general" ? "general" : "trading";
        const badge = mode === "general" ? "普通" : "交易";
        const badgeCls = mode === "general" ? "" : " trading";
        const active = s.is_active ? " is-active" : "";
        const time = esc((s.updated_at || s.created_at || "").slice(0, 16));
        const title = esc(s.title || "新对话");
        const preview = esc(s.preview || "(空会话)");
        const sid = esc(s.id || "");
        return (
          `<div class="ai-chat-history-item${active}" role="listitem" data-session-id="${sid}">` +
          `<div class="ai-chat-history-item-main">` +
          `<span class="ai-chat-history-item-title">${title}</span>` +
          `<span class="ai-chat-history-item-preview">${preview}</span>` +
          `<span class="ai-chat-history-item-meta">` +
          `<span>${time}</span>` +
          `<span class="ai-chat-history-badge${badgeCls}">${badge}</span>` +
          `<span>${Number(s.message_count) || 0} 条</span>` +
          `</span>` +
          `</div>` +
          `<button type="button" class="ai-chat-history-del" title="删除" data-delete-session="${sid}" aria-label="删除">×</button>` +
          `</div>`
        );
      })
      .join("");
  }

  function renderAiChatRow(role, content, extraClass, attachments, rowOpts) {
    const opts = rowOpts || {};
    const botMode = normalizeAiBotMode(opts.botMode || aiSelectedBotMode);
    const isUser = role === "user";
    const isSystem = role === "system";
    let label = "主人";
    if (isSystem) label = "监管";
    else if (!isUser) label = botMode === "general" ? "助手" : botMode === "supervisor" ? "监管AI" : "交易教练";
    const rowCls = isUser
      ? "ai-msg-row-user"
      : isSystem
        ? "ai-msg-row-system"
        : "ai-msg-row-coach";
    const bubbleCls = isUser
      ? "ai-bubble-user"
      : isSystem
        ? "ai-bubble-system"
        : "ai-bubble-assistant";
    const isThinking = extraClass && String(extraClass).includes("ai-bubble-thinking");
    const isError =
      !isUser &&
      !isSystem &&
      !isThinking &&
      /^(AI 调用失败|AI 生成失败)/.test(String(content || "").trim());
    const mdKey =
      !isUser && !isSystem && !isThinking && opts.cacheKey ? String(opts.cacheKey) : "";
    const bubbleInner =
      isUser || isThinking || isSystem ? esc(content || "") : renderHubMarkdown(content || "", mdKey);
    const mdCls = !isUser && !isSystem && !isThinking ? " ai-result-md" : "";
    const attList = Array.isArray(attachments) ? attachments : [];
    const attHtml = attList.length
      ? `<div class="ai-msg-attachments">${attList
          .map((a) => `<span class="ai-attach-chip">${esc(a.name || "附件")}</span>`)
          .join("")}</div>`
      : "";
    const canCopy = !isThinking && String(content || "").trim();
    const copyHtml = canCopy
      ? `<div class="ai-msg-actions"><button type="button" class="ai-msg-copy-btn" data-msg-idx="${opts.msgIdx != null ? opts.msgIdx : ""}">复制</button></div>`
      : "";
    return (
      `<div class="ai-msg-row ${rowCls}">` +
      `<span class="ai-msg-role">${label}</span>` +
      `${attHtml}` +
      `<div class="ai-bubble ${bubbleCls}${mdCls}${isError ? " ai-bubble-error" : ""}${extraClass ? " " + extraClass : ""}">${bubbleInner}</div>` +
      `${copyHtml}` +
      `</div>`
    );
  }

  function renderAiChatMessages(session, opts) {
    const options = opts || {};
    const box = document.getElementById("ai-chat-messages");
    const title = document.getElementById("ai-chat-title");
    if (!box) return;
    const activeSession = isSupervisorMode() ? aiSupervisorSessionCache || session : session;
    const msgs = (activeSession && activeSession.messages) || [];
    const botMode = normalizeAiBotMode((activeSession && activeSession.bot_mode) || aiSelectedBotMode);
    if (title) {
      const modeLabel =
        botMode === "general" ? "普通聊天" : botMode === "supervisor" ? "交易监管" : "交易教练";
      const sessionTitle = activeSession && activeSession.title ? String(activeSession.title) : "";
      if (isMobileAiLayout()) {
        title.textContent =
          botMode === "supervisor"
            ? sessionTitle || "今日监管"
            : sessionTitle && sessionTitle !== "新对话"
              ? sessionTitle
              : modeLabel;
      } else {
        title.textContent = sessionTitle
          ? `${modeLabel} · ${sessionTitle}`
          : modeLabel;
      }
    }
    const showPlaceholder =
      !msgs.length && !options.pendingUser && !options.thinking;
    if (showPlaceholder) {
      const hint =
        botMode === "general"
          ? "普通聊天不注入交易快照;发消息后可点气泡下方「复制」.可粘贴截图或上传附件."
          : botMode === "supervisor"
            ? "今日监管为长会话:手动/中控开平仓与新开仓会自动推送;程序止盈止损会鼓励性提醒.可直接回复继续聊."
            : "交易教练会结合三户监控数据陪聊;发消息后可点气泡下方「复制」.可粘贴截图或点「附件」上传图片/文档.";
      box.innerHTML = `<p class="ai-placeholder">${hint}</p>`;
      return;
    }
    const sessionId = activeSession && activeSession.id ? String(activeSession.id) : "local";
    let html = msgs
      .map((m, idx) => {
        const role = m.role === "user" ? "user" : m.role === "system" ? "system" : "assistant";
        return renderAiChatRow(
          role,
          m.content || "",
          m.level === "warn" ? "ai-bubble-warn" : null,
          m.attachments,
          { botMode, msgIdx: idx, cacheKey: sessionId + ":" + idx }
        );
      })
      .join("");
    if (options.pendingUser) {
      html += renderAiChatRow("user", options.pendingUser, null, options.pendingAttachments);
    }
    if (options.thinking) {
      html += renderAiChatRow("assistant", "正在思考…", "ai-bubble-thinking");
    }
    box.innerHTML = html;
    scrollAiChatToEnd();
  }

  function setAiChatBusy(busy) {
    aiChatLoading = !!busy;
    const btn = document.getElementById("btn-ai-chat-send");
    const input = document.getElementById("ai-chat-input");
    if (btn) btn.disabled = busy;
    if (input) input.disabled = busy;
    document.querySelectorAll(".ai-chat-pending-del").forEach((el) => {
      el.disabled = busy;
    });
  }

  async function loadAiSupervisorSession() {
    const r = await apiFetch("/api/ai/supervisor/session");
    const j = await r.json();
    aiSupervisorSessionCache = j.session || null;
    if (isSupervisorMode()) {
      renderAiChatMessages(aiSupervisorSessionCache);
    }
    updateAiBotTabs("supervisor");
    return j;
  }

  async function switchToSupervisorMode() {
    updateAiBotTabs("supervisor");
    if (isMobileAiLayout()) {
      localStorage.setItem(AI_MOBILE_TAB_KEY, "supervisor");
      applyAiMobileTab("supervisor");
    }
    try {
      await loadAiSupervisorSession();
      connectSupervisorStream();
      scrollAiChatToEnd();
    } catch (e) {
      showToast(String(e), true);
    }
  }

  function closeSupervisorStream() {
    if (supervisorEventSource) {
      supervisorEventSource.close();
      supervisorEventSource = null;
    }
    if (supervisorReconnectTimer) {
      clearTimeout(supervisorReconnectTimer);
      supervisorReconnectTimer = null;
    }
  }

  function connectSupervisorStream() {
    closeSupervisorStream();
    if (currentPage() !== "ai" || !isSupervisorMode()) return;
    supervisorEventSource = new EventSource("/api/ai/supervisor/stream");
    supervisorEventSource.addEventListener("supervisor", (ev) => {
      try {
        const st = JSON.parse(ev.data || "{}");
        const ver = Number(st.supervisor_version) || 0;
        if (ver !== localSupervisorVersion) {
          localSupervisorVersion = ver;
          void loadAiSupervisorSession();
        }
      } catch (_) {}
    });
    supervisorEventSource.onerror = () => {
      closeSupervisorStream();
      if (supervisorReconnectTimer) clearTimeout(supervisorReconnectTimer);
      supervisorReconnectTimer = setTimeout(() => {
        if (currentPage() === "ai" && isSupervisorMode()) connectSupervisorStream();
      }, 8000);
    };
  }

  async function loadAiChatSession() {
    const r = await apiFetch("/api/ai/chat/session");
    const j = await r.json();
    aiChatSessionCache = j.session || null;
    aiChatSessionsCache = j.sessions || [];
    renderAiChatMessages(aiChatSessionCache);
    renderAiChatHistory(aiChatSessionsCache);
    updateAiBotTabs((aiChatSessionCache && aiChatSessionCache.bot_mode) || aiSelectedBotMode);
  }

  async function switchAiChatSession(sessionId) {
    if (!sessionId || aiChatLoading) return;
    try {
      const r = await apiFetch("/api/ai/chat/switch", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ session_id: sessionId }),
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.detail || j.msg || "切换失败");
      aiChatSessionCache = j.session || null;
      aiChatSessionsCache = j.sessions || [];
      renderAiChatMessages(aiChatSessionCache);
      renderAiChatHistory(aiChatSessionsCache);
      const mode =
        (aiChatSessionCache && aiChatSessionCache.bot_mode) === "general" ? "general" : "trading";
      updateAiBotTabs(mode);
      if (isMobileAiLayout()) {
        localStorage.setItem(AI_MOBILE_TAB_KEY, mode);
        applyAiMobileTab(mode);
      }
      scrollAiChatToEnd();
    } catch (e) {
      showToast(String(e), true);
    }
  }

  async function deleteAiChatSession(sessionId) {
    if (!sessionId) return;
    if (!confirm("确定删除这条聊天历史?")) return;
    try {
      const r = await apiFetch(`/api/ai/chat/session/${encodeURIComponent(sessionId)}`, {
        method: "DELETE",
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.detail || j.msg || "删除失败");
      aiChatSessionCache = j.session || null;
      aiChatSessionsCache = j.sessions || [];
      renderAiChatMessages(aiChatSessionCache);
      renderAiChatHistory(aiChatSessionsCache);
      updateAiBotTabs(
        (aiChatSessionCache && aiChatSessionCache.bot_mode) || aiSelectedBotMode || "trading"
      );
      showToast("已删除");
    } catch (e) {
      showToast(String(e), true);
    }
  }

  const ARCHIVE_QUOTE_AI_KEY = "hub_archive_quote_ai";
  let archiveQuoteAiPending = false;

  async function consumeArchiveQuoteAiPending() {
    if (archiveQuoteAiPending || aiChatLoading) return;
    let raw = "";
    try {
      raw = sessionStorage.getItem(ARCHIVE_QUOTE_AI_KEY) || "";
    } catch (_) {
      return;
    }
    if (!raw) return;
    sessionStorage.removeItem(ARCHIVE_QUOTE_AI_KEY);
    let payload;
    try {
      payload = JSON.parse(raw);
    } catch (_) {
      return;
    }
    const content = String((payload && payload.content) || "").trim();
    const quoteDate = String((payload && payload.quote_date) || "").trim();
    if (!content) return;

    const input = document.getElementById("ai-chat-input");
    if (input) input.value = content;
    updateAiBotTabs("trading");
    if (isMobileAiLayout()) {
      localStorage.setItem(AI_MOBILE_TAB_KEY, "trading");
      applyAiMobileTab("trading");
    }

    archiveQuoteAiPending = true;
    setAiChatBusy(true);
    renderAiChatMessages(aiChatSessionCache, {
      pendingUser: content,
      thinking: true,
    });
    try {
      const r = await apiFetch("/api/ai/chat/archive-quote", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ quote_date: quoteDate, content }),
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.detail || j.msg || "发送失败");
      aiChatSessionCache = j.session || null;
      aiChatSessionsCache = j.sessions || aiChatSessionsCache;
      renderAiChatMessages(aiChatSessionCache);
      renderAiChatHistory(aiChatSessionsCache);
      if (input) input.value = "";
      showToast("复盘语录已发送给交易教练");
    } catch (e) {
      showToast(String(e), true);
      if (input) input.value = content;
      try {
        await loadAiChatSession();
      } catch (_) {
        renderAiChatMessages(aiChatSessionCache);
      }
    } finally {
      archiveQuoteAiPending = false;
      setAiChatBusy(false);
    }
  }

  async function loadAiPage() {
    applyAiMobileTab();
    const params = new URLSearchParams(window.location.search || "");
    const modeParam = (params.get("mode") || "").trim().toLowerCase();
    if (modeParam === "supervisor") {
      await switchToSupervisorMode();
    } else {
      closeSupervisorStream();
      await loadAiChatSession();
      await consumeArchiveQuoteAiPending();
    }
    const mobTab = normalizeAiMobileTab(localStorage.getItem(AI_MOBILE_TAB_KEY) || "trading");
    if (isMobileAiLayout() && AI_MOBILE_CHAT_TABS.has(mobTab)) {
      const input = document.getElementById("ai-chat-input");
      if (input && !aiChatLoading) {
        setTimeout(() => input.focus(), 80);
      }
    }
  }

  async function newAiChat(botMode) {
    const mode = normalizeAiBotMode(botMode);
    if (mode !== "supervisor") closeSupervisorStream();
    try {
      const r = await apiFetch("/api/ai/chat/new", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ bot_mode: mode }),
      });
      const j = await r.json();
      aiChatSessionCache = j.session || null;
      aiChatSessionsCache = j.sessions || [];
      renderAiChatMessages(aiChatSessionCache);
      renderAiChatHistory(aiChatSessionsCache);
      updateAiBotTabs(mode);
      if (isMobileAiLayout()) {
        localStorage.setItem(AI_MOBILE_TAB_KEY, mode);
        applyAiMobileTab(mode);
      }
      showToast(
        mode === "general"
          ? "已开始普通聊天"
          : mode === "supervisor"
            ? "已打开今日监管"
            : "已开始交易教练对话"
      );
    } catch (e) {
      showToast(String(e), true);
    }
  }

  async function sendAiChat(ev) {
    if (ev) ev.preventDefault();
    if (aiChatLoading) return;
    const input = document.getElementById("ai-chat-input");
    const text = (input && input.value || "").trim();
    if (isSupervisorMode()) {
      if (!text) return;
      const savedText = text;
      if (input) input.value = "";
      setAiChatBusy(true);
      renderAiChatMessages(aiSupervisorSessionCache, {
        pendingUser: text,
        thinking: true,
      });
      try {
        const r = await apiFetch("/api/ai/supervisor/chat/send", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ message: text }),
        });
        const j = await r.json();
        if (!r.ok) throw new Error(j.detail || j.msg || "发送失败");
        aiSupervisorSessionCache = j.session || null;
        renderAiChatMessages(aiSupervisorSessionCache);
      } catch (e) {
        showToast(String(e), true);
        if (input && savedText) input.value = savedText;
        try {
          await loadAiSupervisorSession();
        } catch (_) {
          renderAiChatMessages(aiSupervisorSessionCache);
        }
      } finally {
        setAiChatBusy(false);
      }
      return;
    }
    const files = aiChatPendingFiles.slice();
    if (!text && !files.length) return;
    const pendingAttachments = files.map((f) => ({
      name: f.name,
      kind: aiChatFileKind(f),
    }));
    const savedText = text;
    if (input) input.value = "";
    setAiChatBusy(true);
    renderAiChatMessages(aiChatSessionCache, {
      pendingUser: text || (files.length ? `(上传 ${files.length} 个附件)` : ""),
      pendingAttachments,
      thinking: true,
    });
    try {
      const fd = new FormData();
      fd.append("message", text);
      files.forEach((f) => fd.append("files", f, f.name));
      const r = await apiFetch("/api/ai/chat/send", { method: "POST", body: fd });
      const j = await r.json();
      if (!r.ok) throw new Error(j.detail || j.msg || "发送失败");
      aiChatSessionCache = j.session || null;
      aiChatSessionsCache = j.sessions || aiChatSessionsCache;
      renderAiChatMessages(aiChatSessionCache);
      renderAiChatHistory(aiChatSessionsCache);
      clearAiChatPendingFiles();
      if (j.attachment_warnings && j.attachment_warnings.length) {
        showToast(j.attachment_warnings.join(";"), true);
      }
    } catch (e) {
      showToast(String(e), true);
      if (input && savedText) input.value = savedText;
      try {
        await loadAiChatSession();
      } catch (_) {
        renderAiChatMessages(aiChatSessionCache);
      }
    } finally {
      setAiChatBusy(false);
    }
  }

  const aiChatFiles = document.getElementById("ai-chat-files");
  if (aiChatFiles) {
    aiChatFiles.addEventListener("change", () => {
      const picked = aiChatFiles.files ? Array.from(aiChatFiles.files) : [];
      addAiChatPendingFiles(picked);
      aiChatFiles.value = "";
    });
  }
  const aiChatInput = document.getElementById("ai-chat-input");
  if (aiChatInput) {
    aiChatInput.addEventListener("paste", handleAiChatPaste);
  }
  const aiChatPending = document.getElementById("ai-chat-pending");
  if (aiChatPending) {
    aiChatPending.addEventListener("click", (ev) => {
      const btn = ev.target.closest("[data-pending-del]");
      if (!btn || aiChatLoading) return;
      ev.preventDefault();
      const idx = Number(btn.getAttribute("data-pending-del"));
      if (!Number.isNaN(idx)) removeAiChatPendingFile(idx);
    });
  }

  const aiChatNewBtn = document.getElementById("btn-ai-chat-new");
  if (aiChatNewBtn) aiChatNewBtn.onclick = () => newAiChat(aiSelectedBotMode);
  const aiChatForm = document.getElementById("ai-chat-form");
  if (aiChatForm) aiChatForm.addEventListener("submit", sendAiChat);

  function initAiChatInteractions() {
    const hist = document.getElementById("ai-chat-history-list");
    if (hist && !hist._aiBound) {
      hist._aiBound = true;
      hist.addEventListener("click", (ev) => {
        const delBtn = ev.target.closest(".ai-chat-history-del");
        if (delBtn) {
          ev.stopPropagation();
          const sid = delBtn.getAttribute("data-delete-session");
          if (sid) deleteAiChatSession(sid);
          return;
        }
        const item = ev.target.closest(".ai-chat-history-item");
        if (!item) return;
        const sid = item.getAttribute("data-session-id");
        if (sid) switchAiChatSession(sid);
      });
    }
    const box = document.getElementById("ai-chat-messages");
    if (box && !box._aiCopyBound) {
      box._aiCopyBound = true;
      box.addEventListener("click", async (ev) => {
        const btn = ev.target.closest(".ai-msg-copy-btn");
        if (!btn) return;
        const idx = Number(btn.getAttribute("data-msg-idx"));
        const msgs = (aiChatSessionCache && aiChatSessionCache.messages) || [];
        const text = msgs[idx] && msgs[idx].content ? String(msgs[idx].content) : "";
        if (!text) return;
        try {
          await navigator.clipboard.writeText(text);
          showToast("已复制");
        } catch (_) {
          showToast("复制失败", true);
        }
      });
    }
    document.querySelectorAll(".ai-bot-tab").forEach((btn) => {
      if (btn._aiBotBound) return;
      btn._aiBotBound = true;
      btn.addEventListener("click", () => {
        const mode = normalizeAiBotMode(btn.getAttribute("data-bot") || "trading");
        if (mode === "supervisor") {
          void switchToSupervisorMode();
          return;
        }
        closeSupervisorStream();
        newAiChat(mode);
      });
    });
  }
  initAiChatInteractions();

  initTpslModal();
  initInstanceFrame();
  initFullscreen();
  initMobileLayout();
  if (globalThis.HubTheme && typeof HubTheme.initToggleUI === "function") {
    HubTheme.initToggleUI();
  }

  function initShellNav() {
    bindHubSpaNavLinks(".top-nav a[href^='/']");
    bindHubSpaNavLinks(".hub-mobile-tabbar a.hub-m-tab[href^='/']");
    bindHubSpaNavLinks(".hub-mobile-more-nav a[href^='/']");
    window.addEventListener("popstate", setActiveNav);
  }

  window.hubNavigateTo = function hubNavigateTo(path) {
    const href = String(path || "/").split("?")[0] || "/";
    if (href === window.location.pathname) {
      setActiveNav();
      return;
    }
    history.pushState({}, "", href);
    setActiveNav();
  };

  window.hubOpenMonitorExpand = function hubOpenMonitorExpand(exId) {
    const id = String(exId || "").trim();
    if (!id) return;
    expandedExchangeId = id;
    sessionStorage.setItem("hub_expanded_ex", id);
    if (currentPage() !== "monitor") {
      history.pushState({}, "", "/monitor");
      setActiveNav();
    }
    if (lastMonitorRows.length) {
      openExchangeFullscreen(id);
    } else {
      void fetchMonitorBoardSnapshot({ showLoading: true });
    }
  };

  initAuth().then((ok) => {
    if (!ok) return;
    initShellNav();
    loadSettings()
      .then((data) => {
        syncDisplayPrefsUI(data);
      })
      .catch(() => {})
      .finally(() => {
        setActiveNav();
      });
  });
  if (window.AccountRiskBadge) AccountRiskBadge.startTicker();
})();
