// Main App — Header (тема + язык + биржа), 2 таба, fetch к /api/ex/{exchange}/*

const REFRESH_INTERVAL = 60; // секунд между light-обновлениями (снижено для anti-abuse)

// =====================================================================
// Root: AuthGate → App. Сначала проверяем сессию через /api/auth/me.
// =====================================================================

const Root = () => {
  const [user, setUser] = useState(null);   // null → не залогинен; объект → залогинен
  const [loading, setLoading] = useState(true);
  const [legacyCreds, setLegacyCreds] = useState(null);   // {ex: {key,secret,pass}} или null

  // Загружаем юзера + server-creds после логина
  const afterLogin = async (u) => {
    setUser(u);
    if (u.is_verified) {
      await window.creds.refresh();
      window.dispatchEvent(new Event("creds-updated"));
      // Если в localStorage остались старые ключи — предложим перенести
      const legacy = window.creds.readLegacy();
      const hasLegacy = Object.keys(legacy).length > 0;
      const ready = window.creds.readyMap();
      // Показываем диалог только если есть локальные ключи которых нет на сервере
      const needMigration = hasLegacy && Object.keys(legacy).some(ex => !ready[ex]);
      if (needMigration) setLegacyCreds(legacy);
    }
  };

  useEffect(() => {
    (async () => {
      try {
        const u = await window.AUTH.me();
        await afterLogin(u);
      } catch (e) {
        setUser(null);
      } finally {
        setLoading(false);
      }
    })();
  }, []);

  const onLogout = async () => {
    try { await window.AUTH.logout(); } catch {}
    setUser(null);
    setLegacyCreds(null);
  };

  if (loading) {
    return (
      <div className="auth-page">
        <div style={{ color: "var(--text-3)", fontFamily: "var(--font-mono)", fontSize: 12 }}>
          Загрузка...
        </div>
      </div>
    );
  }

  if (!user) {
    return <AuthGate onLogin={afterLogin}/>;
  }

  // Две версии desktop UI:
  //   "new" → NEO (handoff Pro Trading, desktop-handoff.jsx) — дефолт
  //   "old" → OG Terminal (оригинальный App до мобильных фишек)
  const uiMode = (window.AlphaUiMode && window.AlphaUiMode.get())
    || (() => { try { return localStorage.getItem("alpha_desktop_ui") || "new"; } catch { return "new"; } })();
  const ActualApp = uiMode === "old" ? App : (window.HandoffApp || window.DesktopApp || App);
  return (
    <>
      <ActualApp user={user} setUser={setUser} onLogout={onLogout}/>
      {legacyCreds && (
        <MigrationModal
          legacy={legacyCreds}
          onDone={() => setLegacyCreds(null)}
          onSkip={() => setLegacyCreds(null)}
        />
      )}
    </>
  );
};

const App = ({ user, setUser, onLogout }) => {
  // --- активная биржа ---
  const [activeEx, setActiveEx] = useState(() => window.creds.active());
  const [accountOpen, setAccountOpen] = useState(false);
  const [adminOpen, setAdminOpen] = useState(false);
  const [telegramOpen, setTelegramOpen] = useState(false);


  // --- theme: "light" / "dark" / "auto" ---
  const [theme, setTheme] = useState(() => {
    try { return localStorage.getItem("alpha_theme") || "auto"; } catch { return "auto"; }
  });
  useEffect(() => {
    const apply = () => {
      let effective = theme;
      if (theme === "auto") {
        effective = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
      }
      document.documentElement.setAttribute("data-theme", effective);
    };
    apply();
    try { localStorage.setItem("alpha_theme", theme); } catch {}
    if (theme === "auto") {
      const m = window.matchMedia("(prefers-color-scheme: dark)");
      const listener = () => apply();
      m.addEventListener("change", listener);
      return () => m.removeEventListener("change", listener);
    }
  }, [theme]);

  // --- tabs ---
  const [tab, setTab] = useState("exchange");
  // Сегмент внутри объединённой вкладки «Боты»: local (API4ATKA) | exchange (с биржи)
  const [botsSeg, setBotsSeg] = useState("local");
  const [settingsOpen, setSettingsOpen] = useState(false);

  // --- state for Exchange tab ---
  const [account, setAccount] = useState({ equity: 0, available: 0, locked: 0, unrealized: 0, margin_ratio: 0 });
  const [positions, setPositions] = useState([]);
  const [orders, setOrders] = useState([]);
  const [closedTrades, setClosedTrades] = useState([]);
  const [analytics, setAnalytics] = useState(null);
  const [prevAnalytics, setPrevAnalytics] = useState(null);  // тот же период, но предыдущий
  const [conn, setConn] = useState("online"); // online / offline / error
  const [lastError, setLastError] = useState(""); // последнее сообщение об ошибке от биржи
  const [hasCreds, setHasCreds] = useState(() => window.creds.hasActive());
  // Обновляем hasCreds когда settings-модалка сохранила/удалила ключи
  useEffect(() => {
    const h = () => setHasCreds(window.creds.hasActive());
    window.addEventListener("creds-updated", h);
    return () => window.removeEventListener("creds-updated", h);
  }, []);
  const [pauseRefresh, setPauseRefresh] = useState(false); // выключаем авто-рефреш во время диагностики
  const [closingPos, setClosingPos] = useState(null); // позиция, для которой открыта модалка закрытия

  // --- period ---
  const [period, setPeriod] = useState("7D");
  const [customRange, setCustomRange] = useState(null); // {from, to} as ISO
  const [tick, setTick] = useState(REFRESH_INTERVAL);
  const [loadingHeavy, setLoadingHeavy] = useState(false);

  // --- helper: build period params ---
  const periodParams = useMemo(() => {
    const params = new URLSearchParams();
    if (customRange) {
      params.set("date_from", customRange.from + "T00:00:00Z");
      params.set("date_to", customRange.to + "T23:59:59Z");
    } else {
      const days = { "1D": 1, "7D": 7, "30D": 30, "90D": 90 }[period] || 7;
      const to = new Date();
      const from = new Date(to.getTime() - days * 86400 * 1000);
      params.set("date_from", from.toISOString());
      params.set("date_to", to.toISOString());
    }
    return params;
  }, [period, customRange]);

  const rangeStr = useMemo(() => {
    const fmt = d => d.toLocaleDateString("en-US", { month: "short", day: "2-digit" });
    let from, to;
    if (customRange) {
      from = new Date(customRange.from);
      to = new Date(customRange.to);
    } else {
      const days = { "1D": 1, "7D": 7, "30D": 30, "90D": 90 }[period] || 7;
      to = new Date();
      from = new Date(to.getTime() - days * 86400 * 1000);
    }
    return fmt(from) + " — " + fmt(to) + ", " + to.getFullYear();
  }, [period, customRange]);

  // ---- fetch helpers ----
  const fetchJson = useCallback(async (url) => {
    const r = await fetch(url, { headers: window.creds.headers() });
    if (!r.ok) {
      const text = await r.text();
      throw new Error("HTTP " + r.status + ": " + text);
    }
    return r.json();
  }, []);

  // ---- Light loop: balance / positions / orders (нормализованные) ----
  const loadLight = useCallback(async () => {
    if (!window.creds.hasActive()) { setConn("error"); return false; }
    const base = "/api/ex/" + activeEx;
    // Если предыдущий цикл был partial/offline — сбрасываем кеш (refresh=true),
    // чтобы UI не путал stale-данные с актуальными
    const bust = (conn === "partial" || conn === "offline") ? "?refresh=true" : "";
    const results = await Promise.allSettled([
      fetchJson(base + "/balance" + bust),
      fetchJson(base + "/positions" + bust),
      fetchJson(base + "/open-orders"),
    ]);
    const okCount = results.filter(r => r.status === "fulfilled").length;

    // Применяем то, что получили
    const balanceData = results[0].status === "fulfilled" ? results[0].value : null;
    const positionsData = results[1].status === "fulfilled" ? results[1].value : null;
    const ordersData = results[2].status === "fulfilled" ? results[2].value : null;

    if (balanceData || positionsData) {
      setAccount(window.ADAPTER.adaptAccount(balanceData, positionsData));
    }
    if (positionsData) setPositions(window.ADAPTER.adaptPositions(positionsData));
    if (ordersData) setOrders(window.ADAPTER.adaptOrders(ordersData));

    // Тристейт: 0 ok = offline, 3 ok = online, иначе partial
    let nextConn;
    if (okCount === 0) nextConn = "offline";
    else if (okCount === 3) nextConn = "online";
    else nextConn = "partial";
    setConn(nextConn);

    // Сообщение об ошибке — про первый упавший endpoint
    if (okCount < 3) {
      const failed = results
        .map((r, i) => ({ name: ["balance", "positions", "open-orders"][i], r }))
        .filter(x => x.r.status === "rejected");
      const msg = failed.length
        ? `${failed.map(f => f.name).join(", ")}: ${String(failed[0].r.reason?.message || failed[0].r.reason || "?")}`
        : "";
      setLastError(msg);
    } else {
      setLastError("");
    }
    return okCount > 0;  // true = биржа хоть что-то отвечает (heavy запускаем)
  }, [fetchJson, activeEx, conn]);

  // ---- Heavy: closed-trades + analytics за период + предыдущий период (для сравнения) ----
  const loadHeavy = useCallback(async () => {
    if (!window.creds.hasActive()) return;
    setClosedTrades([]);
    setAnalytics(null);
    setPrevAnalytics(null);
    setLoadingHeavy(true);
    try {
      const base = "/api/ex/" + activeEx;
      // Вычисляем предыдущий период: ту же длину но раньше
      const from = periodParams.get("date_from");
      const to = periodParams.get("date_to");
      let prevParams = null;
      if (from && to) {
        const fromMs = Date.parse(from);
        const toMs = Date.parse(to);
        const len = toMs - fromMs;
        const prevFrom = new Date(fromMs - len).toISOString();
        const prevTo = new Date(fromMs - 1).toISOString();
        prevParams = new URLSearchParams({ date_from: prevFrom, date_to: prevTo });
      }

      const [closedResp, analyticsResp, prevAnalyticsResp] = await Promise.allSettled([
        fetchJson(base + "/closed-trades?" + periodParams.toString()),
        fetchJson(base + "/analytics?" + periodParams.toString()),
        prevParams ? fetchJson(base + "/analytics?" + prevParams.toString()) : Promise.resolve(null),
      ]);
      if (closedResp.status === "fulfilled") {
        setClosedTrades(window.ADAPTER.adaptClosedTrades(closedResp.value));
      }
      if (analyticsResp.status === "fulfilled") {
        setAnalytics(window.ADAPTER.adaptAnalytics(analyticsResp.value));
      }
      if (prevAnalyticsResp.status === "fulfilled" && prevAnalyticsResp.value) {
        setPrevAnalytics(window.ADAPTER.adaptAnalytics(prevAnalyticsResp.value));
      }
    } finally {
      setLoadingHeavy(false);
    }
  }, [fetchJson, periodParams, activeEx]);

  // ---- Переключение биржи: сброс данных + reload ----
  const switchExchange = useCallback((ex) => {
    if (ex === activeEx) return;
    window.creds.setActive(ex);
    setActiveEx(ex);
    setAccount({ equity: 0, available: 0, locked: 0, unrealized: 0, margin_ratio: 0 });
    setPositions([]);
    setOrders([]);
    setClosedTrades([]);
    setAnalytics(null);
    setHasCreds(window.creds.hasActive());
    // Если новая биржа не поддерживает spot, а мы на spot вкладке — переключаем на exchange
    if (ex === "weex" && tab === "spot") setTab("exchange");
    // Вкладка «Боты» объединяет локальных (любая биржа) и биржевых (OKX) —
    // редирект больше не нужен: внутри сегмент сам скроет биржевой раздел.
  }, [activeEx, tab]);

  // ---- Initial + period change ----
  // Сначала только light. Если он успешен → запускаем heavy. Иначе heavy не
  // дёргаем, чтобы не забивать backend бесполезными запросами к мёртвой бирже.
  useEffect(() => {
    if (!hasCreds) return;
    let cancelled = false;
    (async () => {
      const ok = await loadLight();
      if (cancelled || !ok) return;
      await loadHeavy();
    })();
    return () => { cancelled = true; };
  }, [hasCreds, periodParams, loadLight, loadHeavy]);

  // ---- Light refresh loop ----
  // При offline тормозим до 30с. Во время открытой диагностики совсем выключаем,
  // чтобы фоновые запросы не забивали кеш-локи и не мешали /debug.
  useEffect(() => {
    if (!hasCreds || tab !== "exchange" || pauseRefresh) return;
    const interval = conn === "offline" ? 30 : REFRESH_INTERVAL;
    const id = setInterval(() => {
      setTick(t => {
        if (t <= 1) { loadLight(); return interval; }
        return t - 1;
      });
    }, 1000);
    return () => clearInterval(id);
  }, [hasCreds, tab, loadLight, conn, pauseRefresh]);

  // ---- Equity curve from closed trades ----
  const equityCurve = useMemo(() => {
    return window.ADAPTER.buildEquityCurve(closedTrades, account.equity);
  }, [closedTrades, account.equity]);

  const leaderboard = useMemo(() => {
    return window.ADAPTER.adaptLeaderboard(analytics?.leaderboard);
  }, [analytics]);

  // ---- Cmd+, opens settings ----
  useEffect(() => {
    const h = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key === ",") { e.preventDefault(); setSettingsOpen(true); }
    };
    window.addEventListener("keydown", h);
    return () => window.removeEventListener("keydown", h);
  }, []);

  // ---- If no creds — open settings on mount ----
  useEffect(() => {
    if (!window.creds.hasActive()) setSettingsOpen(true);
  }, []);

  const handleSetPeriod = (p) => { setPeriod(p); setCustomRange(null); };
  const handleApplyCustom = (from, to) => { setCustomRange({ from, to }); setPeriod("Custom"); };

  return (
    <LangProvider>
      <Header
        tab={tab} setTab={setTab}
        conn={conn} tick={tick}
        onRefresh={() => { loadLight(); loadHeavy(); setTick(REFRESH_INTERVAL); }}
        onSettings={() => setSettingsOpen(true)}
        theme={theme} setTheme={setTheme}
        activeEx={activeEx} onSwitchExchange={switchExchange}
        user={user}
        onAccount={() => setAccountOpen(true)}
        onAdmin={() => setAdminOpen(true)}
        onTelegram={() => setTelegramOpen(true)}
      />
      <VerifyBanner user={user} onUpdated={setUser}/>

      <main className="page">
        {tab === "spot" ? (
          <SpotTab
            activeEx={activeEx}
            period={period}
            periodParams={periodParams}
            rangeStr={rangeStr}
            setPeriod={handleSetPeriod}
            applyCustom={handleApplyCustom}
          />
        ) : tab === "bots" ? (
          (() => {
            // Объединённая вкладка: Наши боты (API4ATKA, любая биржа) + С биржи (OKX algo)
            const exSupportsBots = (((window.EXCHANGE_META || {})[activeEx] || {}).supports_bots === true);
            const seg = exSupportsBots ? botsSeg : "local";
            return (
              <>
                <div style={{ display: "flex", gap: 4, margin: "0 0 16px", padding: 4,
                              background: "var(--bg-2, rgba(255,255,255,0.04))",
                              border: "1px solid var(--border, rgba(255,255,255,0.08))",
                              borderRadius: 10, width: "fit-content" }}>
                  <button onClick={() => setBotsSeg("local")}
                    style={{ padding: "7px 18px", borderRadius: 7, border: 0, cursor: "pointer",
                             fontSize: 13, fontWeight: 600,
                             background: seg === "local" ? "var(--accent, #4f8cff)" : "transparent",
                             color: seg === "local" ? "#fff" : "var(--text-2, #aaa)" }}>
                    Наши боты · API4ATKA
                  </button>
                  <button onClick={() => exSupportsBots && setBotsSeg("exchange")}
                    title={exSupportsBots ? "" : "Биржевые алго-боты доступны только на OKX"}
                    style={{ padding: "7px 18px", borderRadius: 7, border: 0,
                             cursor: exSupportsBots ? "pointer" : "not-allowed",
                             fontSize: 13, fontWeight: 600, opacity: exSupportsBots ? 1 : 0.4,
                             background: seg === "exchange" ? "var(--accent, #4f8cff)" : "transparent",
                             color: seg === "exchange" ? "#fff" : "var(--text-2, #aaa)" }}>
                    С биржи · {exSupportsBots ? activeEx.toUpperCase() : "только OKX"}
                  </button>
                </div>
                {seg === "exchange" ? (
                  <BotsTab
                    activeEx={activeEx}
                    period={period}
                    periodParams={periodParams}
                    rangeStr={rangeStr}
                    setPeriod={handleSetPeriod}
                    applyCustom={handleApplyCustom}
                  />
                ) : (
                  window.LocalBotsTab
                    ? <window.LocalBotsTab activeEx={activeEx}/>
                    : <div className="empty"><div className="empty-msg">Локальные боты загружаются…</div></div>
                )}
              </>
            );
          })()
        ) : tab === "exchange" ? (
          <ExchangeTab
            account={account}
            positions={positions}
            orders={orders}
            closedTrades={closedTrades}
            analytics={analytics}
            prevAnalytics={prevAnalytics}
            leaderboard={leaderboard}
            equityCurve={equityCurve}
            period={period}
            setPeriod={handleSetPeriod}
            applyCustom={handleApplyCustom}
            rangeStr={rangeStr}
            conn={conn}
            hasCreds={hasCreds}
            loadingHeavy={loadingHeavy}
            onOpenSettings={() => setSettingsOpen(true)}
            activeEx={activeEx}
            onRetry={async () => {
              const ok = await loadLight();
              if (ok) await loadHeavy();
            }}
            lastError={lastError}
            onPauseRefresh={setPauseRefresh}
            onClosePosition={(p) => setClosingPos(p)}
          />
        ) : tab === "localbots" ? (
          window.LocalBotsTab
            ? <window.LocalBotsTab activeEx={activeEx}/>
            : <div className="empty"><div className="empty-msg">Локальные боты загружаются…</div></div>
        ) : (
          <LocalTradesTab />
        )}
      </main>

      <SettingsModal
        open={settingsOpen}
        onClose={() => setSettingsOpen(false)}
        onSaved={(ex) => {
          if (ex && ex !== activeEx) {
            switchExchange(ex);
          } else {
            setHasCreds(window.creds.hasActive());
            setConn("online");
          }
        }}
        initialExchange={activeEx}
      />

      {closingPos && (
        <ClosePositionModal
          pos={closingPos}
          activeEx={activeEx}
          onClose={() => setClosingPos(null)}
          onDone={() => {
            setClosingPos(null);
            loadLight();  // обновляем позиции после закрытия
          }}
        />
      )}

      {accountOpen && (
        <AccountModal user={user}
          onClose={() => setAccountOpen(false)}
          onLogout={() => { setAccountOpen(false); onLogout(); }}/>
      )}

      {adminOpen && user.role === "admin" && (
        <AdminPanel currentUserId={user.id} onClose={() => setAdminOpen(false)}/>
      )}

      <TelegramSettingsModal open={telegramOpen} onClose={() => setTelegramOpen(false)}/>
    </LangProvider>
  );
};

// ---------- Header ----------
const Header = ({ tab, setTab, conn, tick, onRefresh, onSettings, theme, setTheme, activeEx, onSwitchExchange, user, onAccount, onAdmin, onTelegram }) => {
  const { t } = useLang();
  const connText = conn === "online" ? t("conn_live")
                 : conn === "partial" ? "частично"
                 : conn === "offline" ? t("conn_off")
                 : t("conn_err");
  const connCls = conn === "online" ? "" : conn === "partial" ? "warn"
                : conn === "offline" ? "idle" : "err";
  const exMeta = window.EXCHANGE_META[activeEx] || window.EXCHANGE_META.weex;

  return (
    <header className="app-header">
      <div className="brand">
        <div className="brand-mark">α</div>
        <span>API4ATKA</span>
      </div>

      <nav className="tab-nav">
        <button className={tab === "exchange" ? "active" : ""} onClick={() => setTab("exchange")}>
          <Icon name="activity" size={12} /> {t("tab_exchange")}
        </button>
        {/* Spot вкладка — только для бирж где есть SPOT API (не WEEX) */}
        {(((window.EXCHANGE_META || {})[activeEx] || {}).supports_spot !== false && activeEx !== "weex") && (
          <button className={tab === "spot" ? "active" : ""} onClick={() => setTab("spot")}>
            <Icon name="coins" size={12} /> {t("tab_spot")}
          </button>
        )}
        {/* Объединённая вкладка «Боты»: API4ATKA (любая биржа) + биржевые (OKX) */}
        <button className={tab === "bots" || tab === "localbots" ? "active" : ""} onClick={() => setTab("bots")}>
          <Icon name="activity" size={12} /> {t("tab_bots")}
        </button>
        <button className={tab === "local" ? "active" : ""} onClick={() => setTab("local")}>
          <Icon name="list" size={12} /> {t("tab_local")}
        </button>
      </nav>

      <div className="header-status">
        <ExchangeSwitcher active={activeEx} onChange={onSwitchExchange} />
        <span className="status-chip">
          <span className={"status-dot " + connCls} />
          {exMeta.name} · {connText}
        </span>

        {conn === "online" && (
          <span className="status-chip" title="Auto-refresh">
            <RefreshRing seconds={tick} total={REFRESH_INTERVAL} />
            <span>{t("refresh_in")} {tick}s</span>
          </span>
        )}

        <button className="icon-btn" onClick={onRefresh} title={t("refresh_now")}><Icon name="refresh" size={14} /></button>
        <ThemeToggle theme={theme} setTheme={setTheme} />
        <LangToggle />
        <button className="icon-btn" onClick={onSettings} title={t("settings")}><Icon name="settings" size={14} /></button>
        <button className="icon-btn" onClick={onTelegram} title="Сигналы / торговля"><Icon name="activity" size={14}/></button>
        {user && user.role === "admin" && (
          <button className="icon-btn" onClick={onAdmin} title="Админка"><Icon name="trophy" size={14}/></button>
        )}
        {user && (
          <button className="user-chip" onClick={onAccount} title={user.email}>
            <span className="user-avatar">{(user.email || "?")[0].toUpperCase()}</span>
            <span className="user-email">{user.email}</span>
            {!user.is_verified && <span className="user-dot" title="email не подтверждён"/>}
          </button>
        )}
      </div>
    </header>
  );
};

const RefreshRing = ({ seconds, total }) => {
  const r = 5, c = 2 * Math.PI * r;
  const offset = c * (seconds / total);
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" style={{ flexShrink: 0 }}>
      <circle cx="7" cy="7" r={r} fill="none" stroke="var(--border-2)" strokeWidth="1.5" />
      <circle cx="7" cy="7" r={r} fill="none" stroke="var(--accent)" strokeWidth="1.5"
        strokeDasharray={c} strokeDashoffset={offset} strokeLinecap="round"
        transform="rotate(-90 7 7)"
        style={{ transition: "stroke-dashoffset 0.9s linear" }} />
    </svg>
  );
};

// ---------- Exchange Tab ----------
const ExchangeTab = ({
  account, positions, orders, closedTrades, analytics, prevAnalytics, leaderboard, equityCurve,
  period, setPeriod, applyCustom, rangeStr, conn, hasCreds, loadingHeavy,
  onOpenSettings, activeEx, onRetry, lastError, onPauseRefresh, onClosePosition,
}) => {
  const { t } = useLang();
  const exName = (window.EXCHANGE_META && window.EXCHANGE_META[activeEx]?.name) || (activeEx || "exchange").toUpperCase();
  const subst = (s) => String(s || "").replace(/\{ex\}/g, exName);

  // Парсим HTTP-ошибку: "HTTP 502: {\"detail\":\"Binance HTTP 451: ...\"}"
  // → достаём детальную часть, чтобы показать пользователю
  const parseDetail = (raw) => {
    if (!raw) return "";
    const m = raw.match(/"detail"\s*:\s*"([^"]+)"/);
    if (m) return m[1];
    return raw.length > 200 ? raw.substring(0, 200) + "…" : raw;
  };
  const detailMsg = parseDetail(lastError);

  if (!hasCreds) {
    return (
      <div style={{ padding: "60px 16px" }}>
        <div className="empty" style={{ padding: "40px 32px", maxWidth: 520, margin: "0 auto" }}>
          <div className="empty-icon" style={{ width: 48, height: 48 }}>
            <Icon name="settings" size={20} />
          </div>
          <div className="empty-title" style={{ fontSize: 15 }}>{t("no_creds_title")}</div>
          <div className="empty-msg" style={{ maxWidth: 420 }}>{t("no_creds_msg")}</div>
          <div style={{ display: "flex", gap: 6, marginTop: 14 }}>
            <button className="btn primary" onClick={onOpenSettings}><Icon name="settings" size={12} /> {t("open_settings")}</button>
          </div>
        </div>
      </div>
    );
  }

  if (conn === "offline") {
    return <OfflineState
      exName={exName} subst={subst} detailMsg={detailMsg}
      activeEx={activeEx} onRetry={onRetry} onOpenSettings={onOpenSettings}
      onPauseRefresh={onPauseRefresh}
    />;
  }

  return (
    <>
      {conn === "partial" && (
        <PartialBanner exName={exName} lastError={lastError}
                       parseDetail={parseDetail} onRetry={onRetry}
                       activeEx={activeEx} t={t} />
      )}
      <div className="section"><HeroStrip account={account} /></div>

      <div className="section">
        <div className="section-head">
          <div className="section-title">
            <Icon name="trending" size={11} /> {t("open_positions")}
            <span className="count">{positions.length}</span>
          </div>
          <div className="muted mono" style={{ fontSize: 11 }}>
            {positions.length > 0 && <>{t("total_margin")} <span style={{ color: "var(--text)" }}>{fmtMoney(positions.reduce((s, p) => s + (p.margin || 0), 0))}</span> · {t("unreal_short")} <span className={account.unrealized >= 0 ? "pos" : "neg"}>{fmtSigned(account.unrealized)}</span></>}
          </div>
        </div>
        <PositionsTable positions={positions} onClosePosition={onClosePosition} />
      </div>

      <div className="section">
        <div className="section-head">
          <div className="section-title">
            <Icon name="grid" size={11} /> {t("active_orders")}
            <span className="count">{orders.length}</span>
          </div>
        </div>
        <OrdersGrid orders={orders} />
      </div>

      <div className="section">
        <PeriodBar period={period} setPeriod={setPeriod} range={rangeStr} onApplyCustom={applyCustom} />
        {loadingHeavy && (
          <div className="muted mono" style={{ fontSize: 11, marginTop: 6, textAlign: "right", display: "flex", alignItems: "center", gap: 6, justifyContent: "flex-end" }}>
            <span className="skel-bar" style={{ width: 10, height: 10, borderRadius: "50%" }} />
            {t("loading")}
          </div>
        )}
      </div>

      <div className="section">
        {loadingHeavy ? <SkeletonCards count={4} /> : (
          <AnalyticsRow analytics={analytics} account={account} prevAnalytics={prevAnalytics} />
        )}
      </div>

      {/* Risk metrics */}
      {analytics && !loadingHeavy && analytics.riskMetrics && analytics.total > 0 && (
        <div className="section">
          <div className="section-head">
            <div className="section-title"><Icon name="alert" size={11} /> {t("risk_metrics")}</div>
          </div>
          <RiskMetricsRow riskMetrics={analytics.riskMetrics} />
        </div>
      )}

      {/* Streaks + Size-to-balance */}
      {analytics && !loadingHeavy && analytics.streaks && analytics.total > 0 && (
        <div className="section">
          <div className="section-head">
            <div className="section-title"><Icon name="flame" size={11} /> {t("streaks")}</div>
          </div>
          <StreaksCard streaks={analytics.streaks} />
        </div>
      )}

      {analytics && !loadingHeavy && analytics.sizeToBalance && (
        <div className="section">
          <div className="stats-grid" style={{ gridTemplateColumns: "1fr 3fr" }}>
            <SizeToBalanceCard stb={analytics.sizeToBalance} />
            <div style={{ display: "flex", alignItems: "center", padding: "0 16px", color: "var(--text-3)", fontSize: 12, fontFamily: "var(--font-mono)", lineHeight: 1.6 }}>
              {analytics.sizeToBalance.warning === "high" && (
                <span className="neg">⚠ Высокий риск: больше 80% капитала в позициях. Случайный движ цены = ликвидация.</span>
              )}
              {analytics.sizeToBalance.warning === "medium" && (
                <span style={{ color: "var(--warn)" }}>⚠ Средний риск: больше 50% капитала под маржой.</span>
              )}
              {analytics.sizeToBalance.warning === "ok" && (
                <span className="pos">✓ Риск капитала под контролем.</span>
              )}
            </div>
          </div>
        </div>
      )}

      {/* Long vs Short */}
      {analytics && !loadingHeavy && (analytics.longStats.trades + analytics.shortStats.trades > 0) && (
        <div className="section">
          <div className="section-head">
            <div className="section-title"><Icon name="trending" size={11} /> {t("long_vs_short")}</div>
          </div>
          <LongShortRow longStats={analytics.longStats} shortStats={analytics.shortStats} />
        </div>
      )}

      <div className="section">
        {loadingHeavy ? <SkeletonChart /> : <EquityChart data={equityCurve} period={period} />}
      </div>

      {/* Daily PnL bars */}
      {analytics && !loadingHeavy && analytics.daily && analytics.daily.length > 0 && (
        <div className="section">
          <DailyPnLBars daily={analytics.daily} />
        </div>
      )}

      {/* Месячный календарь активности (заменяет github-стиль heatmap) */}
      {analytics && !loadingHeavy && (
        <div className="section">
          <MonthCalendar daily={analytics.daily} period={period} />
        </div>
      )}

      <div className="section">
        <div className="section-head">
          <div className="section-title">
            <Icon name="barchart" size={11} /> {t("leaderboard")}
          </div>
          <div className="muted mono" style={{ fontSize: 11 }}>{t("by_symbol")} · {period}</div>
        </div>
        {loadingHeavy ? <SkeletonCards count={3} /> : <Leaderboard data={leaderboard} />}
      </div>

      <div className="section">
        <div className="section-head">
          <div className="section-title">
            <Icon name="list" size={11} /> {t("closed_trades")}
            <span className="count">{closedTrades.length}</span>
          </div>
        </div>
        {loadingHeavy ? <SkeletonRows rows={6} cols={9} /> : <ClosedTradesTable trades={closedTrades} />}
      </div>

      <footer style={{ padding: "24px 0 8px", textAlign: "center", color: "var(--text-4)", fontSize: 11, fontFamily: "var(--font-mono)" }}>
        <span>API4ATKA v0.5</span>
        <span style={{ margin: "0 8px" }}>·</span>
        <span>{t("end_of_feed")} · {closedTrades.length} {t("trades_shown")}</span>
      </footer>
    </>
  );
};

// ---------- Partial connection banner (часть endpoint'ов не отвечает) ----------
const PartialBanner = ({ exName, lastError, parseDetail, onRetry, activeEx, t }) => {
  const [diagOpen, setDiagOpen] = useState(false);
  const [diagLoading, setDiagLoading] = useState(false);
  const [diagData, setDiagData] = useState(null);
  const [diagError, setDiagError] = useState("");
  const [diagElapsed, setDiagElapsed] = useState(0);

  const handleDiag = async () => {
    setDiagOpen(true);
    setDiagLoading(true);
    setDiagError("");
    setDiagData(null);
    setDiagElapsed(0);
    const t0 = Date.now();
    const timerId = setInterval(() => setDiagElapsed(((Date.now() - t0) / 1000).toFixed(1)), 200);
    try {
      const ctrl = new AbortController();
      const to = setTimeout(() => ctrl.abort(), 25000);
      const r = await fetch(`/api/ex/${activeEx}/debug`, { headers: window.creds.headers(), signal: ctrl.signal });
      clearTimeout(to);
      setDiagData(await r.json());
    } catch (e) {
      setDiagError(e.name === "AbortError" ? "Превышен таймаут 25с" : (e.message || String(e)));
    } finally {
      clearInterval(timerId);
      setDiagLoading(false);
    }
  };

  return (
    <div className="section">
      <div style={{
        padding: "12px 16px",
        background: "var(--warn-dim)",
        border: "1px solid var(--warn)",
        borderRadius: "var(--r-2)",
        color: "var(--warn)",
        fontSize: 13,
        display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap",
      }}>
        <Icon name="alert" size={14} />
        <div style={{ flex: 1, minWidth: 200 }}>
          <strong>{exName} частично не отвечает.</strong>{" "}
          <span style={{ color: "var(--text-2)", fontSize: 12 }}>
            Часть данных может быть устаревшей.{" "}
            {lastError && <span style={{ fontFamily: "var(--font-mono)", fontSize: 11 }}>({parseDetail(lastError).substring(0, 100)})</span>}
          </span>
        </div>
        <button className="btn" onClick={onRetry} style={{ fontSize: 11, padding: "4px 10px" }}>
          <Icon name="refresh" size={11}/> {t("retry")}
        </button>
        <button className="btn" onClick={handleDiag} disabled={diagLoading} style={{ fontSize: 11, padding: "4px 10px" }}>
          <Icon name="info" size={11}/> {diagLoading ? "..." : "Диагностика"}
        </button>
      </div>

      {diagOpen && (
        <DiagModal
          exchange={activeEx} exName={exName}
          data={diagData} loading={diagLoading} error={diagError}
          elapsed={diagElapsed}
          onClose={() => setDiagOpen(false)}
          onCopy={() => diagData && navigator.clipboard.writeText(JSON.stringify(diagData, null, 2))}
        />
      )}
    </div>
  );
};

// ---------- Offline state with diagnostics modal ----------
const OfflineState = ({ exName, subst, detailMsg, activeEx, onRetry, onOpenSettings, onPauseRefresh }) => {
  const { t } = useLang();
  const [retrying, setRetrying] = useState(false);
  const [diagOpen, setDiagOpen] = useState(false);
  const [diagLoading, setDiagLoading] = useState(false);
  const [diagData, setDiagData] = useState(null);
  const [diagError, setDiagError] = useState("");
  const [diagElapsed, setDiagElapsed] = useState(0);

  const handleRetry = async () => {
    setRetrying(true);
    try { await onRetry(); } finally {
      setTimeout(() => setRetrying(false), 500);
    }
  };

  const handleDiag = async () => {
    onPauseRefresh && onPauseRefresh(true);
    setDiagOpen(true);
    setDiagLoading(true);
    setDiagError("");
    setDiagData(null);
    setDiagElapsed(0);

    const startTs = Date.now();
    const timerId = setInterval(() => {
      setDiagElapsed(((Date.now() - startTs) / 1000).toFixed(1));
    }, 200);

    // Фаза 1: быстрый публичный ping (≤8с). Не требует auth — поэтому
    // не залипает на cache locks и не зависит от ключей.
    try {
      const ctrlPing = new AbortController();
      const tPing = setTimeout(() => ctrlPing.abort(), 8000);
      const r = await fetch(`/api/ex/${activeEx}/ping`, { signal: ctrlPing.signal });
      clearTimeout(tPing);
      const pingData = await r.json();
      // Сразу показываем результат ping, чтобы юзер понял состояние сети
      setDiagData({ public_ping: pingData, calls: {}, diagnosis: pingData.ok
        ? "✓ Сеть до биржи работает. Запускаю auth-проверки..."
        : `⚠ Сеть не достучалась до биржи (${pingData.error_type || pingData.error}). VPN/регион-блок.`
      });
      // Если ping не прошёл — не пытаемся auth (бессмысленно)
      if (!pingData.ok) {
        clearInterval(timerId);
        setDiagLoading(false);
        return;
      }
    } catch (e) {
      const msg = e.name === "AbortError"
        ? "Публичный ping не ответил за 8с. Скорее всего сервер не перезапущен после изменений."
        : (e.message || String(e));
      setDiagError(msg);
      clearInterval(timerId);
      setDiagLoading(false);
      return;
    }

    // Фаза 2: полный /debug с auth-запросами (≤15с)
    try {
      const ctrl = new AbortController();
      const t2 = setTimeout(() => ctrl.abort(), 20000);
      const r = await fetch(`/api/ex/${activeEx}/debug`, {
        headers: window.creds.headers(),
        signal: ctrl.signal,
      });
      clearTimeout(t2);
      const data = await r.json();
      setDiagData(data);
    } catch (e) {
      if (e.name === "AbortError") {
        setDiagError("Полная диагностика не уложилась в 20с. Перезапустите `python main.py` — возможно, в нём старый код.");
      } else {
        setDiagError(e.message || String(e));
      }
    } finally {
      clearInterval(timerId);
      setDiagLoading(false);
    }
  };

  const closeDiag = () => {
    setDiagOpen(false);
    onPauseRefresh && onPauseRefresh(false);  // снова включаем refresh
  };

  const copyDiag = () => {
    if (!diagData) return;
    navigator.clipboard.writeText(JSON.stringify(diagData, null, 2));
  };

  return (
    <>
      <div style={{ padding: "60px 16px" }}>
        <div className="empty" style={{ padding: "40px 32px", maxWidth: 600, margin: "0 auto" }}>
          <div className="empty-icon" style={{ width: 48, height: 48 }}><Icon name="wifi" size={20} /></div>
          <div className="empty-title" style={{ fontSize: 15 }}>{t("offline_title")}</div>
          <div className="empty-msg" style={{ maxWidth: 460 }}>{subst(t("offline_msg"))}</div>

          {detailMsg && (
            <div style={{
              marginTop: 10, padding: "10px 14px",
              background: "var(--bg-2)", border: "1px solid var(--short-dim)",
              borderRadius: "var(--r-2)",
              fontFamily: "var(--font-mono)", fontSize: 11.5,
              color: "var(--short)", lineHeight: 1.5,
              maxWidth: 520, wordBreak: "break-word", textAlign: "left",
            }}>
              <div style={{ color: "var(--text-3)", fontSize: 10, marginBottom: 4, letterSpacing: "0.06em" }}>
                ОТВЕТ БИРЖИ:
              </div>
              {detailMsg}
            </div>
          )}

          <div className="empty-hint" style={{ marginTop: 12, textAlign: "left", maxWidth: 520 }}>
            <div style={{ marginBottom: 6 }}>{subst(t("offline_hint"))}</div>
            {activeEx === "binance" && (
              <div style={{ color: "var(--text-3)", fontSize: 11, lineHeight: 1.6 }}>
                ⚠ <b>Binance активно блокирует commercial VPN</b> (NordVPN, ExpressVPN, Surfshark и др.) —
                независимо от страны выхода. Решения:
                <ul style={{ margin: "4px 0 0 18px", padding: 0 }}>
                  <li>Попробуйте менее известный VPN (Mullvad, ProtonVPN, IVPN) или residential proxy</li>
                  <li>VPS в Сингапуре / Гонконге / Турции / ОАЭ — наиболее надёжно</li>
                  <li>Проверьте напрямую в браузере: <a href={`https://fapi.binance.com/fapi/v1/ping`} target="_blank" rel="noopener noreferrer" style={{ color: "var(--accent)" }}>fapi.binance.com/fapi/v1/ping</a> — если открылось {`{}`}, сеть работает</li>
                </ul>
              </div>
            )}
          </div>
          <div style={{ display: "flex", gap: 6, marginTop: 14, flexWrap: "wrap", justifyContent: "center" }}>
            <button className="btn" onClick={handleRetry} disabled={retrying}>
              <Icon name="refresh" size={12} /> {retrying ? "..." : t("retry")}
            </button>
            <button className="btn" onClick={handleDiag} disabled={diagLoading}>
              <Icon name="info" size={12} /> {diagLoading ? "..." : "Диагностика"}
            </button>
            <button className="btn primary" onClick={onOpenSettings}>
              <Icon name="settings" size={12} /> {t("open_settings")}
            </button>
          </div>
        </div>
      </div>

      {diagOpen && (
        <DiagModal
          exchange={activeEx} exName={exName}
          data={diagData} loading={diagLoading} error={diagError}
          elapsed={diagElapsed}
          onClose={closeDiag} onCopy={copyDiag}
        />
      )}
    </>
  );
};

const DiagModal = ({ exchange, exName, data, loading, error, elapsed, onClose, onCopy }) => {
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" style={{ width: "min(720px, 92vw)", maxHeight: "85vh", display: "flex", flexDirection: "column" }}
        onClick={e => e.stopPropagation()}>
        <div className="modal-head">
          <h2><Icon name="info" size={14} /> Диагностика {exName}</h2>
          <button className="icon-btn" onClick={onClose}><Icon name="x" size={14} /></button>
        </div>
        <div className="modal-body" style={{ overflowY: "auto" }}>
          {loading && (
            <div style={{ padding: 30, textAlign: "center", color: "var(--text-3)" }}>
              <div className="skel-bar" style={{ width: 40, height: 40, borderRadius: "50%", margin: "0 auto 12px" }} />
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 12 }}>
                Ping + 4 auth-запроса параллельно...
              </div>
              <div style={{ fontSize: 12, color: "var(--accent)", marginTop: 6, fontFamily: "var(--font-mono)" }}>
                {elapsed}s
              </div>
              <div style={{ fontSize: 11, color: "var(--text-4)", marginTop: 6 }}>
                Лимит 10 секунд. Если зависнет дольше — точно сетевая проблема.
              </div>
            </div>
          )}
          {error && (
            <div className="empty-hint" style={{ color: "var(--short)" }}>⚠ {error}</div>
          )}
          {data && (
            <>
              {data.diagnosis && (
                <div style={{
                  padding: "10px 14px", marginBottom: 12,
                  background: "var(--bg-2)",
                  border: "1px solid " + (data.public_ping?.ok ? "var(--warn-dim)" : "var(--short-dim)"),
                  borderRadius: "var(--r-2)",
                  fontFamily: "var(--font-mono)", fontSize: 12,
                  color: data.public_ping?.ok ? "var(--warn)" : "var(--short)",
                  lineHeight: 1.5,
                }}>
                  {data.diagnosis}
                </div>
              )}

              {/* Public ping */}
              {data.public_ping && (
                <div style={{ marginBottom: 14 }}>
                  <div style={{ fontSize: 11, color: "var(--text-3)", letterSpacing: "0.06em", fontWeight: 600, textTransform: "uppercase", marginBottom: 6 }}>
                    1. Публичный ping (без авторизации)
                  </div>
                  <DiagRow ok={data.public_ping.ok} seconds={data.public_ping.seconds} content={
                    data.public_ping.ok
                      ? `OK · ${data.public_ping.status} · ${data.public_ping.preview?.substring(0, 80)}…`
                      : `${data.public_ping.error_type || ""} ${data.public_ping.error || ""}`
                  } />
                  <div style={{ fontSize: 10.5, color: "var(--text-4)", marginTop: 4, fontFamily: "var(--font-mono)" }}>
                    URL: {data.public_ping.url}
                  </div>
                </div>
              )}

              {/* Auth calls */}
              {Object.keys(data.calls || {}).length > 0 && (
                <div>
                  <div style={{ fontSize: 11, color: "var(--text-3)", letterSpacing: "0.06em", fontWeight: 600, textTransform: "uppercase", marginBottom: 6 }}>
                    2. Авторизованные вызовы
                  </div>
                  {Object.entries(data.calls).map(([name, info]) => (
                    <div key={name} style={{ marginBottom: 6 }}>
                      <div style={{ fontSize: 11, fontFamily: "var(--font-mono)", color: "var(--text-2)" }}>{name}</div>
                      <DiagRow ok={info.ok} seconds={info.seconds} content={
                        info.ok
                          ? (info.preview || "").substring(0, 200) + (info.preview?.length > 200 ? "…" : "")
                          : (info.error || "?")
                      } />
                    </div>
                  ))}
                </div>
              )}
            </>
          )}
        </div>
        <div className="modal-foot">
          <button className="btn ghost" onClick={onClose}>Закрыть</button>
          {data && (
            <button className="btn" onClick={onCopy}>
              <Icon name="download" size={12} /> Скопировать JSON
            </button>
          )}
        </div>
      </div>
    </div>
  );
};

const DiagRow = ({ ok, seconds, content }) => (
  <div style={{
    padding: "8px 10px",
    background: "var(--bg-2)",
    border: "1px solid " + (ok ? "var(--long-dim)" : "var(--short-dim)"),
    borderRadius: "var(--r-2)",
    fontFamily: "var(--font-mono)", fontSize: 11,
    color: ok ? "var(--text)" : "var(--short)",
    lineHeight: 1.5, wordBreak: "break-word",
    display: "flex", gap: 10, alignItems: "flex-start",
  }}>
    <span style={{
      flex: "0 0 60px",
      color: ok ? "var(--long)" : "var(--short)",
      fontWeight: 600,
    }}>
      {ok ? "OK" : "FAIL"} · {seconds}s
    </span>
    <span style={{ flex: 1 }}>{content}</span>
  </div>
);

// ---------- Close-position confirmation modal ----------
const ClosePositionModal = ({ pos, activeEx, onClose, onDone }) => {
  const { t } = useLang();
  const [sending, setSending] = useState(false);
  const [status, setStatus] = useState({ kind: "", text: "" });
  const [dryRun, setDryRun] = useState(null); // узнаем из ответа
  // Доля закрытия: 0.25 / 0.5 / 0.75 / 1.0
  const [fraction, setFraction] = useState(1.0);
  // Тип ордера: market / limit
  const [orderType, setOrderType] = useState("market");
  // Лимитная цена (для limit)
  const [limitPrice, setLimitPrice] = useState(() => pos.mark || pos.entry || 0);

  const closeQty = +(pos.size * fraction).toPrecision(10);

  const handleConfirm = async () => {
    setSending(true);
    setStatus({ kind: "muted", text: "..." });
    try {
      const body = {
        symbol: pos.sym + "USDT",
        side: pos.side,
        qty: closeQty,
        order_type: orderType,
      };
      if (orderType === "limit") {
        const lp = parseFloat(limitPrice);
        if (!lp || lp <= 0) throw new Error("Лимит-цена должна быть > 0");
        body.limit_price = lp;
      }
      const r = await fetch(`/api/ex/${activeEx}/close-position`, {
        method: "POST",
        headers: { ...window.creds.headers(), "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const data = await r.json();
      if (!r.ok) throw new Error(data.detail || `HTTP ${r.status}`);
      setDryRun(!!data.dry_run);
      setStatus({ kind: "ok", text: data.dry_run
        ? "🟡 " + t("close_pos_done") + " (DRY_RUN)"
        : "✅ " + t("close_pos_done")
      });
      setTimeout(() => onDone(), 1500);
    } catch (e) {
      setStatus({ kind: "err", text: "❌ " + t("close_pos_failed") + ": " + (e.message || String(e)) });
    } finally {
      setSending(false);
    }
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()} style={{ width: "min(480px, 92vw)" }}>
        <div className="modal-head">
          <h2><Icon name="alert" size={14} style={{ color: "var(--short)" }} /> {t("close_pos_title")}</h2>
          <button className="icon-btn" onClick={onClose}><Icon name="x" size={14} /></button>
        </div>
        <div className="modal-body">
          <div style={{
            padding: "14px 16px", marginBottom: 12,
            background: "var(--bg-2)", border: "1px solid var(--border-2)",
            borderRadius: "var(--r-2)",
          }}>
            <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8 }}>
              <CoinIcon sym={pos.sym} size={22} />
              <span className="sym" style={{ fontSize: 14 }}>{pos.sym}-PERP</span>
              <SidePill side={pos.side} />
              <span className="mono muted" style={{ fontSize: 11 }}>{pos.leverage}×</span>
            </div>
            <div style={{ fontSize: 12, color: "var(--text-2)", fontFamily: "var(--font-mono)", lineHeight: 1.7 }}>
              <div>Размер: <b>{pos.size}</b> {pos.sym}</div>
              <div>Цена входа: <b>{fmtPrice(pos.entry)}</b></div>
              <div>Текущая: <b>{fmtPrice(pos.mark)}</b></div>
              <div>Нереализ. PnL: <b className={pos.pnl >= 0 ? "pos" : "neg"}>{fmtSigned(pos.pnl)}</b> ({fmtPct(pos.pnlPct)})</div>
            </div>
          </div>

          {/* Доля закрытия */}
          <div style={{ marginBottom: 10 }}>
            <div style={{ fontSize: 11, color: "var(--text-3)", marginBottom: 6,
              textTransform: "uppercase", letterSpacing: 0.5 }}>Закрыть часть</div>
            <div className="segmented" style={{ display: "flex", gap: 4 }}>
              {[0.25, 0.5, 0.75, 1.0].map(f => (
                <button key={f}
                  className={fraction === f ? "active" : ""}
                  onClick={() => setFraction(f)}
                  style={{ flex: 1, padding: "8px 4px" }}>
                  {f === 1.0 ? "100%" : Math.round(f * 100) + "%"}
                </button>
              ))}
            </div>
            <div style={{ fontSize: 11, color: "var(--text-3)", marginTop: 6,
              fontFamily: "var(--font-mono)" }}>
              qty: <b style={{ color: "var(--text)" }}>{closeQty}</b> {pos.sym}
            </div>
          </div>

          {/* Тип ордера */}
          <div style={{ marginBottom: 10 }}>
            <div style={{ fontSize: 11, color: "var(--text-3)", marginBottom: 6,
              textTransform: "uppercase", letterSpacing: 0.5 }}>Тип ордера</div>
            <div className="segmented" style={{ display: "flex", gap: 4 }}>
              <button className={orderType === "market" ? "active" : ""}
                onClick={() => setOrderType("market")}
                style={{ flex: 1, padding: "8px 4px" }}>Market</button>
              <button className={orderType === "limit" ? "active" : ""}
                onClick={() => setOrderType("limit")}
                style={{ flex: 1, padding: "8px 4px" }}>Limit</button>
            </div>
            {orderType === "limit" && (
              <input type="number" step="any" value={limitPrice}
                onChange={e => setLimitPrice(e.target.value)}
                style={{
                  marginTop: 8, width: "100%", padding: "8px 10px",
                  background: "var(--bg-2)", border: "1px solid var(--border-2)",
                  borderRadius: "var(--r-1)", color: "var(--text)",
                  fontFamily: "var(--font-mono)", fontSize: 13,
                }}
                placeholder="Лимит-цена" />
            )}
          </div>

          <div style={{ fontSize: 13, color: "var(--text-2)", lineHeight: 1.6 }}>
            {t("close_pos_confirm")}
          </div>

          <div className="empty-hint" style={{ marginTop: 10, textAlign: "left", color: "var(--warn)", borderColor: "var(--warn-dim)" }}>
            {dryRun === null
              ? t("close_pos_real") + " " + t("close_pos_dry").substring(0, 80) + "..."
              : dryRun ? t("close_pos_dry") : t("close_pos_real")}
          </div>

          {status.text && (
            <div style={{ marginTop: 10, fontSize: 12, fontFamily: "var(--font-mono)",
              color: status.kind === "ok" ? "var(--long)" :
                     status.kind === "err" ? "var(--short)" : "var(--text-3)" }}>
              {status.text}
            </div>
          )}
        </div>
        <div className="modal-foot">
          <button className="btn ghost" onClick={onClose} disabled={sending}>{t("close_pos_cancel")}</button>
          <button className="btn danger" onClick={handleConfirm} disabled={sending}
            style={{ background: "var(--short)", color: "white", borderColor: "var(--short)" }}>
            {sending ? "..." : <><Icon name="x" size={12} /> {t("close_pos_btn")}</>}
          </button>
        </div>
      </div>
    </div>
  );
};

// ---------- Error Boundary ----------
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null, info: null };
  }
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  componentDidCatch(error, info) {
    console.error("ErrorBoundary caught:", error, info);
    this.setState({ info });
  }
  render() {
    if (this.state.hasError) {
      return (
        <div style={{
          padding: 40, maxWidth: 800, margin: "40px auto",
          background: "#1a0f0f", border: "1px solid #6b1c1c", borderRadius: 8,
          fontFamily: "monospace", color: "#fff5f5",
        }}>
          <h2 style={{ color: "#fb7185", marginTop: 0 }}>⚠ Ошибка React</h2>
          <div style={{ fontSize: 13, marginBottom: 12, color: "#ffd6d6" }}>
            Что-то сломалось в UI. Скопируйте текст ниже и пришлите:
          </div>
          <pre style={{
            background: "#0a0a0a", padding: 12, borderRadius: 4,
            overflow: "auto", maxHeight: 300, fontSize: 12, lineHeight: 1.4,
            whiteSpace: "pre-wrap", wordBreak: "break-word",
          }}>
{String(this.state.error?.stack || this.state.error || "Unknown error")}
{this.state.info?.componentStack ? "\n\n--- componentStack ---\n" + this.state.info.componentStack : ""}
          </pre>
          <button
            onClick={() => window.location.reload()}
            style={{
              marginTop: 12, padding: "8px 16px", background: "#4f8cff",
              border: 0, color: "white", borderRadius: 4, cursor: "pointer",
            }}
          >⟳ Перезагрузить страницу</button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Mount: на мобильных (≤640px) рендерим MobileApp, иначе обычный Desktop Root.
// Mobile рендерится только если юзер авторизован (как и Desktop) — иначе
// показываем auth screens которые сами адаптивны.
const MobileGate = () => {
  const [user, setUser] = React.useState(undefined);   // undefined=loading, null=guest
  const [mobileReady, setMobileReady] = React.useState(!!window.MobileApp);

  React.useEffect(() => {
    fetch("/api/auth/me", { credentials: "include" })
      .then(r => r.ok ? r.json() : null)
      .then(setUser)
      .catch(() => setUser(null));
  }, []);

  // Babel компилирует mobile-app.jsx асинхронно. При первой загрузке
  // window.MobileApp может ещё не существовать → показывали бы десктоп <Root />.
  // Слушаем событие "mobile-app-ready" из mobile-app.jsx + fallback-poll каждые 50мс.
  React.useEffect(() => {
    if (mobileReady) return;
    if (window.MobileApp) { setMobileReady(true); return; }
    const onReady = () => setMobileReady(true);
    window.addEventListener("mobile-app-ready", onReady, { once: true });
    const id = setInterval(() => {
      if (window.MobileApp) { setMobileReady(true); clearInterval(id); }
    }, 50);
    return () => {
      window.removeEventListener("mobile-app-ready", onReady);
      clearInterval(id);
    };
  }, [mobileReady]);

  // Гость на мобильном видит desktop-auth (<Root/>). Как только залогинится —
  // перезагружаем страницу, чтобы переключиться на мобильный UI (а не остаться в десктопе).
  React.useEffect(() => {
    if (user !== null) return;
    const id = setInterval(() => {
      fetch("/api/auth/me", { credentials: "include" })
        .then(r => (r.ok ? r.json() : null))
        .then(u => { if (u) { clearInterval(id); window.location.reload(); } })
        .catch(() => {});
    }, 1200);
    return () => clearInterval(id);
  }, [user]);

  // Пока загружается auth ИЛИ MobileApp для залогиненного — показываем спиннер
  // (а НЕ десктоп-Root, иначе будет визуальный flash).
  if (user === undefined || (user && !mobileReady)) {
    return <div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100dvh", color: "#7a7a7a" }}>Загрузка…</div>;
  }
  if (user === null) {
    // Гость → desktop-AUTH (он адаптивный для мобилы через auth.jsx)
    return <Root />;
  }
  return <window.MobileApp />;
};

// Надёжный детект мобильного: узкий вьюпорт ИЛИ тач-устройство до 1024px.
// Тач-проверка ловит телефоны в режиме «версия для ПК» (ширина спуфится ~980,
// но pointer остаётся coarse) — раньше такие попадали в десктоп-UI.
const _mq = (q) => (window.matchMedia ? window.matchMedia(q).matches : false);
const isMobile = _mq("(max-width: 760px)") || (_mq("(pointer: coarse)") && _mq("(max-width: 1024px)"));
ReactDOM.createRoot(document.getElementById("root")).render(
  <ErrorBoundary>{isMobile ? <MobileGate /> : <Root />}</ErrorBoundary>
);
