/* eslint-disable */
/* =========================================================================
   API4ATKA · Desktop Handoff Bundle
   ------------------------------------------------------------------------
   Один большой IIFE — содержит все Pro-Trading-Dark компоненты из
   design_handoff_api4atka_desktop/, объединённые так чтобы не конфликтовать
   с мобильным/старым desktop в глобальном скоупе.
   Экспорт: window.HandoffApp (root компонент).
   Правки относительно оригинала handoff:
     • Боты — сегмент «Наши боты / С биржи», мастер создания начинается с
       выбора ТИПА (Сигнальный / Grid / DCA / Basket / Recurring), канал
       нужен только для Сигнального.
     • Сигналы — 3 подвкладки (Каналы / Live лента / Перенаправления).
     • Биржа — торговый терминал (позиции, ордера, баланс), а не управление
       API-ключами (это уехало в Настройки → API).
     • Дашборд — добавлен Verify-email banner.
     • Журнал — добавлен фильтр по source (manual/signal/bot/copy-trade).
   ========================================================================= */
(function () {
const { useState, useEffect, useRef, useMemo, useCallback } = React;

/* ===================== LIVE DATA LAYER (real API) ======================= */
const API = () => window.MobileAPI;

// useLiveData(fetcher, fallback, deps=[], pollMs=0) → [data, loading, refresh]
// При первом рендере отдаёт fallback (mock из HDB) и параллельно fetch'ит реальные данные.
// Если API недоступен (нет MobileAPI / fetch упал) — остаёмся на fallback, UI не падает.
const useLiveData = (fetcher, fallback, deps = [], pollMs = 0) => {
  const [data, setData] = useState(fallback);
  const [loading, setLoading] = useState(true);
  const fetchRef = useRef(fetcher);
  fetchRef.current = fetcher;
  const refresh = useCallback(async () => {
    if (!API()) { setLoading(false); return; }
    try {
      const v = await fetchRef.current();
      if (v != null) setData(v);
    } catch (e) { /* keep fallback */ }
    setLoading(false);
  }, []);
  useEffect(() => {
    let mounted = true;
    let timer;
    const tick = async () => {
      if (!mounted) return;
      if (!API()) { setLoading(false); return; }
      try {
        const v = await fetchRef.current();
        if (mounted && v != null) setData(v);
      } catch (e) { /* keep prior */ }
      if (mounted) setLoading(false);
    };
    tick();
    if (pollMs > 0) timer = setInterval(tick, pollMs);
    return () => { mounted = false; if (timer) clearInterval(timer); };
  }, deps);
  return [data, loading, refresh];
};

// Мапперы: формат MobileAPI → формат handoff UI
const mapPosition = (p) => ({
  id: p.id || ('p_' + p.sym),
  symbol: p.sym || p.symbol,
  side: String(p.side || 'LONG').toLowerCase(),
  size: (p.qty || 0) * (p.mark || p.entry || 0), // USDT notional
  qty: p.qty || 0,                                // raw qty (для close)
  lev: p.lev || 1,
  entry: p.entry,
  mark: p.mark,
  liq: p.liq || 0,
  margin: p.margin || 0,
  pnl: p.pnl || 0,
  pnlPct: p.pnlPct || 0,
  source: p.channel || 'ручная',
});

const mapLocalBot = (b) => {
  // Бэк (list_local_bots) отдаёт: state, bot_type, symbol, exchange, direction,
  // leverage, investment_usdt, total_pnl, trades_count, config{}, created_at,
  // started_at, stopped_at, live{mark_px, unrealized_pnl, ...}.
  const cfg = b.config || {};
  const live = b.live || {};
  const invest = parseFloat(b.investment_usdt || 0);
  const realized = parseFloat(b.total_pnl || 0);
  const unrealized = parseFloat(live.unrealized_pnl || 0);
  const pnl = realized + unrealized;
  // время работы — от started_at (если запущен) или created_at
  const startTs = b.started_at || b.created_at;
  const refTs = (b.state === 'running') ? Date.now() : (b.stopped_at || Date.now());
  const days = startTs ? Math.max(0, Math.floor((refTs - startTs) / 86400000)) : 0;
  return {
    id: b.id,
    name: b.symbol || ('Bot ' + b.id),
    symbol: b.symbol || '',
    type: String(b.bot_type || '').toLowerCase(),
    kind: 'local',
    exchange: (b.exchange || 'WEEX').toUpperCase(),
    direction: (b.direction || 'long').toLowerCase(),
    status: (b.state || 'stopped').toLowerCase(),   // ← было b.status (всегда undefined → stopped)
    invest,
    pnl,
    realized,
    unrealized,
    pnlPct: invest > 0 ? (pnl / invest * 100) : 0,
    days,
    startTs,
    lev: b.leverage || cfg.leverage || live.actual_leverage || 1,
    gridCount: cfg.grid_num || cfg.grid_count || b.grid_count || null,
    tradesCount: b.trades_count || 0,
    markPx: live.mark_px || 0,
    entryPx: live.entry_price || 0,
    posSize: live.position_size || 0,
    liqPx: live.liquidation_price || 0,
    source: b.signal_source || null,
    config: cfg,
  };
};

const mapExchangeBot = (b) => ({
  id: 'eb_' + (b.algoId || b.instId || b.id || Math.random()),
  algoId: b.algoId || b.id || null,
  algoOrdType: b.algoOrdType || b.type || 'contract_grid',
  name: ((b.instId || b.symbol || 'Algo').replace(/[-_]/g, '').replace('SWAP', '')) + ' · OKX Grid',
  symbol: (b.instId || b.symbol || '').replace(/[-_]/g, '').replace('SWAP', ''),
  type: 'okx-' + (b.algoOrdType || 'grid'),
  kind: 'exchange',
  exchange: 'OKX',
  status: (b.state || b.status || 'stopped').toLowerCase(),
  invest: parseFloat(b.investment || 0),
  pnl: parseFloat(b.totalPnl || b.runtime?.pnl || 0),
  pnlPct: parseFloat(b.pnlRatio || 0) * 100,
  days: (b.cTime && b.uTime) ? Math.max(0, Math.round((parseFloat(b.uTime) - parseFloat(b.cTime)) / 86400000)) : 0,
  lev: parseInt(b.lever || 1),
});

const mapSpotHolding = (h) => ({
  sym: h.symbol || h.coin || h.asset,
  name: h.name || h.symbol || h.coin,
  amount: parseFloat(h.amount || h.balance || h.total || 0),
  price: parseFloat(h.price || h.value_usdt / (h.amount || 1) || 0),
  value: parseFloat(h.value_usdt || h.usdt_value || 0),
  pct: parseFloat(h.pct || 0),
  change: parseFloat(h.change_24h || h.change || 0),
});

const mapTrade = (t) => ({
  id: t.id || t.tradeId,
  symbol: t.symbol || t.sym,
  side: String(t.side || 'LONG').toLowerCase(),
  lev: t.lev || 1,
  source: t.channel || t.source || 'ручная',
  sourceKind: t.botManaged ? 'bot' : (t.source === 'bot' ? 'bot' : (t.channel ? 'signal' : 'manual')),
  entry: t.entry,
  exit: t.exit,
  pnl: t.pnl || 0,
  pnlPct: t.pnlPct || 0,
  win: (t.pnl || 0) > 0,
  closed: t.closedAt ? new Date(t.closedAt).getTime() : Date.now(),
  dur: t.duration ? Math.round(t.duration / 60) + 'м' : '',
});

const mapChannel = (c) => {
  // API возвращает: { id, channel_ref ("@foo" или "-100…"), channel_id, title, label, enabled, ... }
  const name = c.label || c.title || c.channel_ref || ('Канал ' + (c.id || ''));
  const tg = c.channel_ref || (c.channel_id ? String(c.channel_id) : '');
  return {
    id: c.id || c.channel_id,
    name, tg,
    channel_ref: c.channel_ref || '',
    label: c.label || '',
    on: !!(c.enabled ?? c.is_active ?? c.on),
    signals30: c.signals_30d || c.signals_count || c.total_signals || 0,
    winRate: c.win_rate || c.wr || 0,
    pnl30: c.pnl_30d || c.total_pnl || c.pnl_usdt || 0,
    accent: c.color || '#3B82F6',
    leverage: c.leverage || c.default_leverage || 5,
    // бэк отдаёт default_entry_type / default_sl_pct
    entry_type: (c.default_entry_type || c.entry_type || 'signal'),
    sl_pct: (c.default_sl_pct != null ? c.default_sl_pct : (c.sl_pct != null ? c.sl_pct : 0)),
  };
};

const mapAccount = (a) => ({
  id: a.id || a.account_id || a.exchange,
  name: (a.exchange || 'EXC').toUpperCase(),
  status: a.has_creds || a.active ? 'connected' : 'disconnected',
  balance: parseFloat(a.balance || 0),
  key: a.api_key_mask || (a.has_creds ? '••••' : null),
  perms: a.has_creds ? ['Чтение', 'Торговля'] : [],
  primary: !!a.primary,
  label: a.label || '',
});



/* ===================== ICONS ============================================ */
const ICONS = {
  dashboard: 'M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z',
  signal: ['M4.93 19.07a10 10 0 010-14.14','M19.07 4.93a10 10 0 010 14.14','M7.76 16.24a6 6 0 010-8.49','M16.24 7.76a6 6 0 010 8.49','M12 12.01'],
  exchange: ['M16 3l4 4-4 4','M20 7H4','M8 21l-4-4 4-4','M4 17h16'],
  spot: ['M21 12c0 1.66-4 3-9 3s-9-1.34-9-3','M3 5c0-1.66 4-3 9-3s9 1.34 9 3v14c0 1.66-4 3-9 3s-9-1.34-9-3V5','M3 12c0 1.66 4 3 9 3s9-1.34 9-3'],
  bot: ['M12 8V4H8','M2 14h2','M20 14h2','M15 13v2','M9 13v2','M4 8h16a1 1 0 011 1v10a1 1 0 01-1 1H4a1 1 0 01-1-1V9a1 1 0 011-1z'],
  copytrade: ['M8 4h10a2 2 0 012 2v10','M16 8H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2V10a2 2 0 00-2-2z','M8 14l2.5 2.5L16 11'],
  journal: ['M4 19.5A2.5 2.5 0 016.5 17H20','M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z','M9 7h7','M9 11h7'],
  stats: ['M3 3v18h18','M18 9l-5 5-3-3-3 3'],
  settings: ['M12.22 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2h.44a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0l-.43-.25a2 2 0 01-1-1.73V4a2 2 0 00-2-2z','M12 12m-3 0a3 3 0 106 0a3 3 0 10-6 0'],
  admin: ['M20 7h-9','M14 17H5','M17 17m-3 0a3 3 0 106 0a3 3 0 10-6 0','M7 7m-3 0a3 3 0 106 0a3 3 0 10-6 0'],
  bell: ['M6 8a6 6 0 0112 0c0 7 3 9 3 9H3s3-2 3-9','M10.3 21a1.94 1.94 0 003.4 0'],
  sun: ['M12 12m-4 0a4 4 0 108 0a4 4 0 10-8 0','M12 2v2','M12 20v2','M4.93 4.93l1.41 1.41','M17.66 17.66l1.41 1.41','M2 12h2','M20 12h2','M6.34 17.66l-1.41 1.41','M19.07 4.93l-1.41 1.41'],
  moon: 'M12 3a6 6 0 009 9 9 9 0 11-9-9z',
  chevdown: 'M6 9l6 6 6-6',
  chevright: 'M9 6l6 6-6 6',
  chevleft: 'M15 6l-6 6 6 6',
  refresh: ['M3 12a9 9 0 019-9 9.75 9.75 0 016.74 2.74L21 8','M21 3v5h-5','M21 12a9 9 0 01-9 9 9.75 9.75 0 01-6.74-2.74L3 16','M3 21v-5h5'],
  search: ['M11 11m-8 0a8 8 0 1016 0a8 8 0 10-16 0','M21 21l-4.3-4.3'],
  plus: ['M12 5v14','M5 12h14'],
  check: 'M20 6L9 17l-5-5',
  checkcircle: ['M12 12m-10 0a10 10 0 1020 0a10 10 0 10-20 0','M9 12l2 2 4-4'],
  x: ['M18 6L6 18','M6 6l12 12'],
  fire: 'M12 2c1 5-3 6-3 10a3 3 0 006 0c0-1.5-1-2-1-3 2 1 4 3 4 6a6 6 0 01-12 0c0-5 4-7 6-13z',
  shield: ['M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z'],
  warning: ['M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z','M12 9v4','M12 17h.01'],
  target: ['M12 12m-10 0a10 10 0 1020 0a10 10 0 10-20 0','M12 12m-6 0a6 6 0 1012 0a6 6 0 10-12 0','M12 12m-2 0a2 2 0 104 0a2 2 0 10-4 0'],
  download: ['M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4','M7 10l5 5 5-5','M12 15V3'],
  forward: ['M15 17l5-5-5-5','M4 18v-2a4 4 0 014-4h12'],
  crown: ['M2 4l3 12h14l3-12-6 7-4-7-4 7-6-7z','M5 20h14'],
  trophy: ['M6 9H4.5a2.5 2.5 0 010-5H6','M18 9h1.5a2.5 2.5 0 000-5H18','M4 22h16','M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22','M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22','M18 2H6v7a6 6 0 0012 0V2z'],
  star: 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z',
  mail: ['M22 7l-10 5L2 7','M4 4h16a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2z'],
  trash: ['M3 6h18','M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2','M10 11v6','M14 11v6'],
  edit: ['M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7','M18.5 2.5a2.12 2.12 0 013 3L12 15l-4 1 1-4 9.5-9.5z'],
  money: ['M12 1v22','M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6'],
  copy: ['M9 9h10a2 2 0 012 2v10a2 2 0 01-2 2H9a2 2 0 01-2-2V11a2 2 0 012-2z','M5 15H4a2 2 0 01-2-2V3a2 2 0 012-2h10a2 2 0 012 2v1'],
  master: ['M12 12m-4 0a4 4 0 108 0a4 4 0 10-8 0','M6 21v-1a4 4 0 014-4h4a4 4 0 014 4v1','M19 3l1 2 2 1-2 1-1 2-1-2-2-1 2-1z'],
  globe: ['M12 12m-10 0a10 10 0 1020 0a10 10 0 10-20 0','M2 12h20','M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z'],
  lock: ['M5 11h14a2 2 0 012 2v7a2 2 0 01-2 2H5a2 2 0 01-2-2v-7a2 2 0 012-2z','M7 11V7a5 5 0 0110 0v4'],
  logout: ['M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4','M16 17l5-5-5-5','M21 12H9'],
  layers: ['M12 2L2 7l10 5 10-5-10-5z','M2 17l10 5 10-5','M2 12l10 5 10-5'],
  telegram: ['M22 2L11 13','M22 2l-7 20-4-9-9-4 20-7z'],
  key: ['M15.5 7.5m-1.5 0a1.5 1.5 0 103 0a1.5 1.5 0 10-3 0','M21 2l-9.6 9.6','M15.5 7.5L10 13l-3 3-2 1-1 2 1 1 2-1 1-2 3-2 5.5-5.5'],
  trendup: ['M22 7l-8.5 8.5-5-5L2 17','M16 7h6v6'],
  trenddown: ['M22 17l-8.5-8.5-5 5L2 7','M16 17h6v-6'],
  pause: ['M6 4h4v16H6z','M14 4h4v16h-4z'],
  play: 'M6 3l14 9-14 9V3z',
  stop: 'M5 5h14v14H5z',
  filter: 'M22 3H2l8 9.46V19l4 2v-8.54L22 3z',
  calendar: ['M8 2v4','M16 2v4','M3 10h18','M5 4h14a2 2 0 012 2v14a2 2 0 01-2 2H5a2 2 0 01-2-2V6a2 2 0 012-2z'],
  clock: ['M12 12m-10 0a10 10 0 1020 0a10 10 0 10-20 0','M12 6v6l4 2'],
  user: ['M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2','M12 7m-4 0a4 4 0 108 0a4 4 0 10-8 0'],
  arrowright: ['M5 12h14','M12 5l7 7-7 7'],
  zap: 'M13 2L3 14h9l-1 8 10-12h-9l1-8z',
  grid: ['M3 3h7v7H3z','M14 3h7v7h-7z','M14 14h7v7h-7z','M3 14h7v7H3z'],
  percent: ['M19 5L5 19','M6.5 6.5m-2.5 0a2.5 2.5 0 105 0a2.5 2.5 0 10-5 0','M17.5 17.5m-2.5 0a2.5 2.5 0 105 0a2.5 2.5 0 10-5 0'],
  coffee: ['M17 8h1a4 4 0 010 8h-1','M3 8h14v9a4 4 0 01-4 4H7a4 4 0 01-4-4V8z','M6 2v2','M10 2v2','M14 2v2'],
  wallet: ['M21 12V7H5a2 2 0 010-4h14v4','M3 5v14a2 2 0 002 2h16v-5','M18 12a2 2 0 000 4h4v-4h-4z'],
  scale: ['M12 3v18','M5 7h14','M5 7l-3 7a4 4 0 008 0L5 7z','M19 7l-3 7a4 4 0 008 0l-3-7z','M7 21h10'],
  info: ['M12 12m-10 0a10 10 0 1020 0a10 10 0 10-20 0','M12 16v-4','M12 8h.01'],
  more: ['M12 12m-1 0a1 1 0 102 0a1 1 0 10-2 0','M12 5m-1 0a1 1 0 102 0a1 1 0 10-2 0','M12 19m-1 0a1 1 0 102 0a1 1 0 10-2 0'],
  link: ['M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71','M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71'],
  flame: 'M12 2c1 5-3 6-3 10a3 3 0 006 0c0-1.5-1-2-1-3 2 1 4 3 4 6a6 6 0 01-12 0c0-5 4-7 6-13z',
  briefcase: ['M20 7H4a2 2 0 00-2 2v10a2 2 0 002 2h16a2 2 0 002-2V9a2 2 0 00-2-2z','M16 7V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v2'],
  eye: ['M2 12s4-8 10-8 10 8 10 8-4 8-10 8-10-8-10-8z','M12 12m-3 0a3 3 0 106 0a3 3 0 10-6 0'],
};

function Icon({ name, size = 18, stroke = 2, style, className }) {
  const d = ICONS[name];
  if (!d) return null;
  const paths = Array.isArray(d) ? d : [d];
  return (
    <svg width={size} height={size} viewBox="0 0 24 24"
      fill={name === 'dashboard' ? 'currentColor' : 'none'}
      stroke={name === 'dashboard' ? 'none' : 'currentColor'}
      strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round"
      className={className} style={style}>
      {paths.map((p, i) => <path key={i} d={p} />)}
    </svg>
  );
}

/* ===================== HELPERS / COMPONENTS ============================= */
const fmt = {
  usd: (n, d = 2) => (n < 0 ? '-' : '') + '$' + Math.abs(n).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d }),
  num: (n, d = 2) => n.toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d }),
  usdt: (n, d = 2) => (n < 0 ? '-' : '') + Math.abs(n).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d }),
  pct: (n, d = 1) => (n > 0 ? '+' : '') + n.toFixed(d) + '%',
  sign: (n) => (n > 0 ? '+' : ''),
  compact: (n) => Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 }).format(n),
};
const cx = (...a) => a.filter(Boolean).join(' ');
const pn = (n) => (n > 0 ? 'pos' : n < 0 ? 'neg' : '');
const _agoStr = (ts) => {
  const dt = new Date(ts);
  if (isNaN(dt)) return '';
  const mins = Math.round((Date.now() - dt.getTime()) / 60000);
  return mins < 1 ? 'только что'
       : mins < 60 ? `${mins} мин назад`
       : mins < 1440 ? `${Math.floor(mins / 60)} ч назад`
       : `${Math.floor(mins / 1440)} д назад`;
};

function Avatar({ name, hue = 210, size = 40, square = false }) {
  const initials = (name || '?').split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
  return (
    <div style={{
      width: size, height: size, flex: 'none', borderRadius: square ? size * 0.28 : '50%',
      background: `linear-gradient(135deg, hsl(${hue} 70% 52%), hsl(${(hue + 40) % 360} 65% 42%))`,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      color: '#fff', fontWeight: 700, fontSize: size * 0.36, letterSpacing: '-0.02em',
    }}>{initials}</div>
  );
}

function Sparkline({ data, w = 160, h = 36, color, fill = true, strokeW = 1.8 }) {
  const vals = (data || []).map(d => typeof d === 'number' ? d : d.v);
  if (vals.length < 2) return <svg width={w} height={h}></svg>;
  const min = Math.min(...vals), max = Math.max(...vals);
  const span = max - min || 1;
  const stepX = w / (vals.length - 1);
  const pts = vals.map((v, i) => [i * stepX, h - ((v - min) / span) * (h - 4) - 2]);
  const line = pts.map((p, i) => (i === 0 ? 'M' : 'L') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ');
  const up = vals[vals.length - 1] >= vals[0];
  const c = color || (up ? 'var(--green)' : 'var(--red)');
  const id = useMemo(() => 'sp' + Math.random().toString(36).slice(2, 8), []);
  const area = `${line} L ${w} ${h} L 0 ${h} Z`;
  return (
    <svg width={w} height={h} className="kpi-spark" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none">
      {fill && <defs><linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stopColor={c} stopOpacity="0.28" /><stop offset="100%" stopColor={c} stopOpacity="0" />
      </linearGradient></defs>}
      {fill && <path d={area} fill={`url(#${id})`} />}
      <path d={line} fill="none" stroke={c} strokeWidth={strokeW} strokeLinejoin="round" strokeLinecap="round" />
    </svg>
  );
}

function AreaChart({ data, height = 260, color = 'var(--accent)' }) {
  const ref = useRef(null);
  useEffect(() => {
    if (!ref.current || !window.LightweightCharts) return;
    // ВАЖНО: читаем переменные с самого элемента (а не с :root) — при
    // переключении версий сайта тема может быть scoped на обёртке, и на :root
    // переменная вернётся пустой. И на всё ставим фолбэк: пустой цвет роняет
    // lightweight-charts ("Cannot parse color: ").
    const css = getComputedStyle(ref.current);
    const pick = (name, fb) => {
      const v = (css.getPropertyValue(name) || '').trim();
      return v || fb;
    };
    let accent = color.startsWith('var') ? pick(color.slice(4, -1), '') : color;
    if (!/^#|^rgb|^hsl/i.test(accent)) accent = '#4D7CFE';
    const txt = pick('--text-muted', 'rgba(138,145,166,0.8)');
    const grid = pick('--border', 'rgba(255,255,255,0.06)');
    const chart = window.LightweightCharts.createChart(ref.current, {
      width: ref.current.clientWidth, height,
      layout: { background: { color: 'transparent' }, textColor: txt, fontFamily: 'JetBrains Mono, monospace', fontSize: 11 },
      grid: { vertLines: { visible: false }, horzLines: { color: grid } },
      rightPriceScale: { borderVisible: false },
      timeScale: { borderVisible: false, timeVisible: true },
      crosshair: { mode: 1, vertLine: { labelBackgroundColor: accent }, horzLine: { labelBackgroundColor: accent } },
      handleScroll: false, handleScale: false,
    });
    const series = chart.addAreaSeries({
      lineColor: accent, topColor: accent + '44', bottomColor: accent + '03',
      lineWidth: 2, priceLineVisible: false, lastValueVisible: false,
    });
    series.setData(data.map(d => ({ time: Math.floor(d.t / 1000), value: d.v })));
    chart.timeScale().fitContent();
    const ro = new ResizeObserver(() => { if (ref.current) chart.applyOptions({ width: ref.current.clientWidth }); });
    ro.observe(ref.current);
    return () => { ro.disconnect(); chart.remove(); };
  }, [data, height, color]);
  return <div ref={ref} style={{ width: '100%' }} />;
}

function Donut({ segments, size = 132, thickness = 16, center }) {
  const total = segments.reduce((s, x) => s + x.value, 0) || 1;
  const r = (size - thickness) / 2;
  const c = 2 * Math.PI * r;
  let offset = 0;
  return (
    <div style={{ position: 'relative', width: size, height: size }}>
      <svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }}>
        {segments.map((s, i) => {
          const len = (s.value / total) * c;
          const el = <circle key={i} cx={size / 2} cy={size / 2} r={r} fill="none"
            stroke={s.color} strokeWidth={thickness} strokeDasharray={`${len} ${c - len}`}
            strokeDashoffset={-offset} />;
          offset += len; return el;
        })}
      </svg>
      {center && <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 2 }}>{center}</div>}
    </div>
  );
}

function WinBar({ wins, losses }) {
  const total = wins + losses || 1;
  return (
    <div style={{ display: 'flex', height: 6, borderRadius: 99, overflow: 'hidden', gap: 2 }}>
      <div style={{ width: `${wins / total * 100}%`, background: 'var(--green)' }} />
      <div style={{ width: `${losses / total * 100}%`, background: 'var(--red)' }} />
    </div>
  );
}

function Kpi({ label, value, sub, delta, spark, sparkColor, icon, accent }) {
  return (
    <div className="card card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
      <div className="row between">
        <span className="label-cap">{label}</span>
        {icon && <span style={{ color: accent || 'var(--text-muted)' }}><Icon name={icon} size={16} /></span>}
      </div>
      <div className="row" style={{ gap: 8, alignItems: 'baseline' }}>
        <span className="mono" style={{ fontSize: 26, fontWeight: 700, letterSpacing: '-0.02em' }}>{value}</span>
        {delta != null && <span className={cx('mono', pn(delta))} style={{ fontSize: 13, fontWeight: 600 }}>{fmt.pct(delta)}</span>}
      </div>
      {sub && <div className="muted" style={{ fontSize: 12.5 }}>{sub}</div>}
      {spark && <div style={{ marginTop: 'auto' }}><Sparkline data={spark} w={240} h={34} color={sparkColor} /></div>}
    </div>
  );
}

function Modal({ title, sub, onClose, children, footer, width = 540 }) {
  useEffect(() => {
    const h = e => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h);
  }, [onClose]);
  return (
    <div className="scrim" onMouseDown={onClose}>
      <div className="modal" style={{ maxWidth: width }} onMouseDown={e => e.stopPropagation()}>
        <div className="modal-head">
          <div>
            <div style={{ fontSize: 16.5, fontWeight: 700 }}>{title}</div>
            {sub && <div className="muted" style={{ fontSize: 12.5, marginTop: 3 }}>{sub}</div>}
          </div>
          <button className="btn btn-ghost btn-icon btn-sm" onClick={onClose}><Icon name="x" size={16} /></button>
        </div>
        <div className="modal-body">{children}</div>
        {footer && <div className="modal-foot">{footer}</div>}
      </div>
    </div>
  );
}

function Empty({ icon = 'coffee', title, text, action }) {
  return (
    <div className="empty">
      <Icon name={icon} size={40} stroke={1.6} />
      <div style={{ fontSize: 16, fontWeight: 700, color: 'var(--text)' }}>{title}</div>
      {text && <div style={{ fontSize: 13.5, maxWidth: 340, lineHeight: 1.5 }}>{text}</div>}
      {action && <div style={{ marginTop: 12 }}>{action}</div>}
    </div>
  );
}

function StatusPill({ status }) {
  const map = {
    running: { c: 'var(--green)', t: 'Работает' }, active: { c: 'var(--green)', t: 'Активен' },
    online: { c: 'var(--green)', t: 'online' }, connected: { c: 'var(--green)', t: 'Подключено' },
    paused: { c: 'var(--amber)', t: 'Пауза' }, pending: { c: 'var(--amber)', t: 'Ожидание' },
    stopped: { c: 'var(--red)', t: 'Остановлен' }, disconnected: { c: 'var(--text-muted)', t: 'Не подключено' },
    filled: { c: 'var(--green)', t: 'Исполнен' }, skipped: { c: 'var(--text-muted)', t: 'Пропущен' },
    open: { c: 'var(--green)', t: 'Открыта' }, closed: { c: 'var(--text-muted)', t: 'Закрыта' },
    failed: { c: 'var(--red)', t: 'Ошибка' },
  };
  const s = map[status] || { c: 'var(--text-muted)', t: status };
  return <span className="pill pill-soft"><span className="dot" style={{ background: s.c }} />{s.t}</span>;
}

const ToastCtx = React.createContext(() => {});
function ToastHost({ children }) {
  const [toasts, setToasts] = useState([]);
  const push = useCallback((text, type = 'info') => {
    const id = Math.random().toString(36).slice(2);
    setToasts(t => [...t, { id, text, type }]);
    setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3200);
  }, []);
  return (
    <ToastCtx.Provider value={push}>
      {children}
      <div style={{ position: 'fixed', bottom: 24, right: 24, zIndex: 200, display: 'flex', flexDirection: 'column', gap: 10 }}>
        {toasts.map(t => (
          <div key={t.id} className="card fade-in" style={{ padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 10, boxShadow: 'var(--shadow)', minWidth: 240 }}>
            <span style={{ color: t.type === 'error' ? 'var(--red)' : t.type === 'success' ? 'var(--green)' : 'var(--accent)' }}>
              <Icon name={t.type === 'error' ? 'warning' : t.type === 'success' ? 'checkcircle' : 'info'} size={18} />
            </span>
            <span style={{ fontSize: 13.5, fontWeight: 500 }}>{t.text}</span>
          </div>
        ))}
      </div>
    </ToastCtx.Provider>
  );
}
const useToast = () => React.useContext(ToastCtx);

function SectionHead({ title, count, action, children }) {
  return (
    <div className="row between" style={{ marginBottom: 14 }}>
      <div className="row" style={{ gap: 10 }}>
        <h2 style={{ margin: 0, fontSize: 15, fontWeight: 700 }}>{title}</h2>
        {count != null && <span className="pill pill-soft mono">{count}</span>}
      </div>
      <div className="row" style={{ gap: 8 }}>{action || children}</div>
    </div>
  );
}

/* ===================== MOCK DB =========================================== */
const rng = (seed => () => (seed = (seed * 16807) % 2147483647) / 2147483647)(987654);
const jit = (base, amp) => base + (rng() - 0.5) * amp;
function seriesGen(start, points, vol, drift) {
  const out = []; let v = start;
  const now = 1717000000000;
  for (let i = points - 1; i >= 0; i--) {
    v = Math.max(1, v + (rng() - 0.5) * vol + drift);
    out.push({ t: now - i * 3600 * 1000, v: +v.toFixed(2) });
  }
  return out;
}
const balanceSeries = seriesGen(5210, 60, 28, -0.6).map((p, i, a) =>
  i === a.length - 1 ? { ...p, v: 5170.98 } : p
);
const HDB = {
  USER: { name: 'scared_trader', email: 'trader@api4atka.io', emailVerified: false, plan: 'PRO', isAdmin: true },
  EXCHANGE: { name: 'WEEX', status: 'online', balance: 5170.98, available: 5170.98, equity: 5170.98, pnlDay: -46.99, pnlDayPct: -0.9, tradesDay: 7, wins: 2, losses: 5, winRate: 29 },
  CHANNELS: [
    { id: 'mozart', name: 'MOZART', tg: '-1003734159802', on: true, signals30: 184, winRate: 64, pnl30: 1284.5, accent: '#3B82F6', leverage: 10 },
    { id: 'signalflow', name: 'The Signal Flow', tg: '-1001909340554', on: true, signals30: 96, winRate: 58, pnl30: 612.3, accent: '#14B8A6', leverage: 5 },
    { id: 'ixoma', name: 'iXOMA', tg: '-1003053771580', on: true, signals30: 142, winRate: 71, pnl30: 2103.8, accent: '#A855F7', leverage: 20 },
    { id: 'scp', name: 'SCP Trade FUTURES', tg: '-1003804602693', on: false, signals30: 71, winRate: 49, pnl30: -218.4, accent: '#F59E0B', leverage: 8 },
  ],
  POSITIONS: [
    { id: 'p1', symbol: 'TONUSDT', side: 'long', size: 412.5, lev: 10, entry: 5.284, mark: 5.361, liq: 4.802, margin: 41.25, pnl: 6.01, pnlPct: 14.6, source: 'iXOMA' },
    { id: 'p2', symbol: 'BTCUSDT', side: 'short', size: 980.0, lev: 20, entry: 68420, mark: 68910, liq: 71880, margin: 49.0, pnl: -7.02, pnlPct: -14.3, source: 'MOZART' },
  ],
  ORDERS: [
    { id: 'o1', symbol: 'ETHUSDT', side: 'long', type: 'LIMIT', price: 3420.5, qty: 0.5, status: 'NEW' },
    { id: 'o2', symbol: 'SOLUSDT', side: 'short', type: 'STOP', price: 160.8, qty: 4, status: 'NEW' },
  ],
  TRADES: Array.from({ length: 40 }, (_, i) => {
    const syms = ['TONUSDT','BTCUSDT','ETHUSDT','SOLUSDT','SUIUSDT','OPUSDT','ARBUSDT','DOGEUSDT'];
    const chs = ['MOZART','iXOMA','The Signal Flow','SCP Trade FUTURES'];
    const sources_kind = ['signal', 'manual', 'bot', 'copy'];
    const sym = syms[Math.floor(rng()*syms.length)];
    const side = rng() > 0.5 ? 'long' : 'short';
    const win = rng() > 0.42;
    const pnl = +((win ? 1 : -1) * (rng()*38 + 2)).toFixed(2);
    const pnlPct = +(pnl / (rng()*40+30) * 10).toFixed(1);
    return {
      id: 't' + (i + 1), symbol: sym, side,
      lev: [3, 5, 10, 20][Math.floor(rng() * 4)],
      source: chs[Math.floor(rng() * chs.length)],
      sourceKind: sources_kind[Math.floor(rng() * sources_kind.length)],
      entry: +(rng() * 100 + 1).toFixed(4),
      exit: +(rng() * 100 + 1).toFixed(4),
      pnl, pnlPct, win,
      closed: 1717000000000 - i * 3600 * 1000 * jit(4, 3),
      dur: Math.floor(rng() * 240 + 8) + 'м',
    };
  }),
  SIGNALS: Array.from({ length: 9 }, (_, i) => {
    const syms = ['TONUSDT','BTCUSDT','ETHUSDT','SOLUSDT','WIFUSDT','PEPEUSDT','NEARUSDT'];
    const chsAccents = [{ name: 'MOZART', accent: '#3B82F6', lev: 10 }, { name: 'iXOMA', accent: '#A855F7', lev: 20 }, { name: 'The Signal Flow', accent: '#14B8A6', lev: 5 }];
    const ch = chsAccents[Math.floor(rng() * chsAccents.length)];
    const side = rng() > 0.5 ? 'long' : 'short';
    const sym = syms[Math.floor(rng() * syms.length)];
    const entry = +(rng() * 100 + 1).toFixed(4);
    return {
      id: 's' + (i + 1), symbol: sym, side, source: ch.name, accent: ch.accent,
      entry, lev: ch.lev,
      tps: [+(entry * 1.02).toFixed(4), +(entry * 1.05).toFixed(4), +(entry * 1.09).toFixed(4)],
      sl: +(entry * 0.97).toFixed(4),
      status: ['filled', 'filled', 'pending', 'skipped'][Math.floor(rng() * 4)],
      ago: Math.floor(rng() * 55 + 1) + 'м назад',
    };
  }),
  // ВАЖНО: 2 категории ботов — наши (kind:'local') и биржевые (kind:'exchange')
  LOCAL_BOTS: [
    { id: 'lb1', name: 'TON Grid', type: 'grid', kind: 'local', symbol: 'TONUSDT', exchange: 'OKX', status: 'running', invest: 199.86, pnl: -14.67, pnlPct: -7.34, days: 15, gridCount: 24, lev: 10 },
    { id: 'lb2', name: 'BTC DCA', type: 'dca', kind: 'local', symbol: 'BTCUSDT', exchange: 'WEEX', status: 'running', invest: 300, pnl: 18.4, pnlPct: 6.13, days: 6, lev: 1, safeties: 3 },
    { id: 'lb3', name: 'Майская корзина', type: 'basket', kind: 'local', symbol: 'BASKET-4', exchange: 'WEEX', status: 'paused', invest: 500, pnl: 12.3, pnlPct: 2.46, days: 12, lev: 1 },
    { id: 'lb4', name: 'iXOMA Signal Bot', type: 'signal', kind: 'local', symbol: 'iXOMA канал', exchange: 'WEEX', status: 'running', invest: 1000, pnl: 142.8, pnlPct: 14.28, days: 30, lev: 20, source: 'iXOMA' },
  ],
  EXCHANGE_BOTS: [
    { id: 'eb1', name: 'OKX Auto Grid · TONUSDT', type: 'okx-grid', kind: 'exchange', symbol: 'TONUSDT', exchange: 'OKX', status: 'running', invest: 250, pnl: 6.2, pnlPct: 2.48, days: 8, lev: 5 },
  ],
  MASTERS: Array.from({ length: 6 }, (_, i) => {
    const names = ['iXOMA Capital', 'MOZART Fund', 'Delta Neutral', 'SCP Aggressive', 'Signal Flow', 'Quiet Alpha'];
    return {
      id: 'm' + (i + 1), name: names[i],
      avatarHue: Math.floor(rng() * 360),
      roi30: +(rng() * 180 + 20).toFixed(1),
      roi7: +(rng() * 40 - 5).toFixed(1),
      winRate: Math.floor(rng() * 30 + 55),
      followers: Math.floor(rng() * 4000 + 120),
      aum: Math.floor(rng() * 900000 + 40000),
      maxDd: +(rng() * 22 + 4).toFixed(1),
      pf: +(rng() * 2 + 1.2).toFixed(2),
      trades90: Math.floor(rng() * 600 + 80),
      sharpe: +(rng() * 2 + 0.8).toFixed(2),
      fee: [10, 12, 15, 18, 20][Math.floor(rng() * 5)],
      minCopy: [50, 100, 150, 200][Math.floor(rng() * 4)],
      verified: rng() > 0.4,
      tags: i % 2 ? ['Фьючерсы', 'Скальпинг'] : ['Свинг', 'Низкий риск'],
      curve: seriesGen(1000, 40, 30, 8).map(p => p.v),
      bio: 'Системный интрадей по альткоинам. Жёсткий риск-менеджмент.',
    };
  }),
  MY_COPIES: [
    { masterId: 'm1', master: 'iXOMA Capital', allocated: 1000, pnl: 284.6, pnlPct: 28.5, since: '12 дн', status: 'active', copyOpen: 1 },
    { masterId: 'm5', master: 'Signal Flow', allocated: 500, pnl: 38.2, pnlPct: 7.6, since: '6 дн', status: 'active', copyOpen: 0 },
  ],
  NOTIFS: [
    { id: 'n1', type: 'fill', text: 'iXOMA Signal Bot открыл LONG TONUSDT ×10', ago: '42 мин', unread: true },
    { id: 'n2', type: 'signal', text: 'Новый сигнал MOZART: SHORT BTCUSDT', ago: '18 мин', unread: true },
    { id: 'n3', type: 'warn', text: 'SCP Futures остановлен: серия из 3 убытков', ago: '2 ч', unread: false },
    { id: 'n4', type: 'copy', text: 'iXOMA Capital закрыл сделку: +24.6 USDT', ago: '5 ч', unread: false },
  ],
  SPOT: [
    { sym: 'USDT', name: 'Tether', amount: 3120.50, price: 1, value: 3120.50, pct: 60.3, change: 0 },
    { sym: 'BTC', name: 'Bitcoin', amount: 0.018, price: 68910, value: 1240.38, pct: 24.0, change: 1.8 },
    { sym: 'TON', name: 'Toncoin', amount: 96.4, price: 5.361, value: 516.8, pct: 10.0, change: 4.2 },
    { sym: 'SOL', name: 'Solana', amount: 1.84, price: 159.4, value: 293.3, pct: 5.7, change: -2.1 },
  ],
  CONNECTIONS: [
    { id: 'weex', name: 'WEEX', status: 'connected', balance: 5170.98, key: 'wx_live_••••8f21', perms: ['Чтение', 'Торговля'], primary: true },
    { id: 'okx', name: 'OKX', status: 'connected', balance: 1245.32, key: 'okx_live_••••f02a', perms: ['Чтение', 'Торговля'], primary: false },
    { id: 'bybit', name: 'Bybit', status: 'disconnected', balance: 0, key: null, perms: [], primary: false },
    { id: 'binance', name: 'Binance', status: 'disconnected', balance: 0, key: null, perms: [], primary: false },
  ],
  balanceSeries, series: seriesGen,
};

/* ===================== SHELL ============================================ */
const NAV = [
  { id: 'dashboard', label: 'Дашборд', icon: 'dashboard' },
  { id: 'bots', label: 'Боты', icon: 'bot' },
  { id: 'copytrade', label: 'Копи-трейдинг', icon: 'copytrade', badge: 'NEW' },
  { id: 'signals', label: 'Сигналы', icon: 'signal' },
  { id: 'journal', label: 'Журнал', icon: 'journal' },
  { id: 'spot', label: 'Спот', icon: 'spot' },
  { id: 'stats', label: 'Статистика', icon: 'stats' },
  { id: 'settings', label: 'Настройки', icon: 'settings' },
];

function useHashRoute() {
  const parse = () => {
    const h = window.location.hash.replace(/^#\/?/, '') || 'dashboard';
    const [page, ...rest] = h.split('/');
    return { page: page || 'dashboard', params: rest };
  };
  const [route, setRoute] = useState(parse());
  useEffect(() => {
    const on = () => setRoute(parse());
    window.addEventListener('hashchange', on);
    return () => window.removeEventListener('hashchange', on);
  }, []);
  const nav = useCallback((to) => { window.location.hash = '#/' + to; }, []);
  return [route, nav];
}

function Logo({ collapsed }) {
  return (
    <div className="mono" style={{ fontWeight: 700, fontSize: 17, letterSpacing: '0.04em', display: 'flex', alignItems: 'center', gap: 1, userSelect: 'none' }}>
      {collapsed ? <span>A<span style={{ color: 'var(--accent)' }}>4</span></span>
        : <span>API<span style={{ color: 'var(--accent)' }}>4</span>ATKA</span>}
    </div>
  );
}

function Sidebar({ route, nav, isAdmin, collapsed, setCollapsed }) {
  return (
    <aside style={{
      width: collapsed ? 68 : 'var(--sidebar-w)', flex: 'none', borderRight: '1px solid var(--border)',
      background: 'var(--bg-1)', display: 'flex', flexDirection: 'column', transition: 'width .18s',
    }}>
      <div style={{ height: 'var(--header-h)', display: 'flex', alignItems: 'center', justifyContent: collapsed ? 'center' : 'space-between', padding: collapsed ? 0 : '0 18px', borderBottom: '1px solid var(--border)' }}>
        <Logo collapsed={collapsed} />
      </div>
      <nav style={{ flex: 1, padding: '12px 12px', display: 'flex', flexDirection: 'column', gap: 2, overflowY: 'auto' }}>
        {NAV.map(n => {
          const on = route.page === n.id;
          return (
            <button key={n.id} onClick={() => nav(n.id)} title={collapsed ? n.label : ''}
              style={{
                display: 'flex', alignItems: 'center', gap: 12, height: 42, padding: collapsed ? 0 : '0 12px',
                justifyContent: collapsed ? 'center' : 'flex-start',
                borderRadius: 'var(--radius-sm)', fontSize: 14, fontWeight: on ? 600 : 500,
                color: on ? 'var(--text)' : 'var(--text-2)',
                background: on ? 'var(--accent-soft)' : 'transparent', position: 'relative', transition: '.12s',
              }}>
              {on && <span style={{ position: 'absolute', left: 0, top: 10, bottom: 10, width: 3, borderRadius: 99, background: 'var(--accent)' }} />}
              <span style={{ color: on ? 'var(--accent)' : 'var(--text-muted)', flex: 'none' }}><Icon name={n.icon} size={19} /></span>
              {!collapsed && <span style={{ flex: 1, textAlign: 'left' }}>{n.label}</span>}
              {!collapsed && n.badge && <span className="badge badge-accent">{n.badge}</span>}
            </button>
          );
        })}
        {isAdmin && (() => {
          const on = route.page === 'admin';
          return (
            <button onClick={() => nav('admin')} title={collapsed ? 'Админ' : ''}
              style={{ display: 'flex', alignItems: 'center', gap: 12, height: 42, padding: collapsed ? 0 : '0 12px', justifyContent: collapsed ? 'center' : 'flex-start', borderRadius: 'var(--radius-sm)', fontSize: 14, fontWeight: on ? 600 : 500, color: on ? 'var(--text)' : 'var(--text-2)', background: on ? 'var(--accent-soft)' : 'transparent', position: 'relative', marginTop: 8 }}>
              {on && <span style={{ position: 'absolute', left: 0, top: 10, bottom: 10, width: 3, borderRadius: 99, background: 'var(--accent)' }} />}
              <span style={{ color: on ? 'var(--accent)' : 'var(--text-muted)', flex: 'none' }}><Icon name="admin" size={19} /></span>
              {!collapsed && <span style={{ flex: 1, textAlign: 'left' }}>Админ</span>}
            </button>
          );
        })()}
      </nav>
      <div style={{ padding: 12, borderTop: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 8 }}>
        {/* Переключатель NEO ⇄ OG Terminal */}
        <button
          title="Переключиться на OG Terminal (классический интерфейс)"
          onClick={() => {
            if (window.AlphaUiMode) window.AlphaUiMode.set('old');
            else { try { localStorage.setItem('alpha_desktop_ui', 'old'); } catch {} }
            setTimeout(() => window.location.reload(), 80);
          }}
          className="btn btn-ghost btn-sm" style={{ width: '100%', justifyContent: collapsed ? 'center' : 'flex-start' }}>
          <Icon name="layers" size={15} />
          {!collapsed && (
            <span style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', lineHeight: 1.2 }}>
              <span style={{ fontSize: 12, fontWeight: 700 }}>NEO <span className="badge badge-emerald" style={{ marginLeft: 4, fontSize: 8.5 }}>ВКЛ</span></span>
              <span className="muted" style={{ fontSize: 10 }}>→ сменить на OG Terminal</span>
            </span>
          )}
        </button>
        <button onClick={() => setCollapsed(c => !c)} className="btn btn-ghost btn-sm" style={{ width: '100%' }}>
          <Icon name={collapsed ? 'chevright' : 'chevleft'} size={16} />
          {!collapsed && <span>Свернуть</span>}
        </button>
      </div>
    </aside>
  );
}

function fmtAgo(ts) {
  if (!ts) return '';
  const s = Math.max(0, (Date.now() - Number(ts)) / 1000);
  if (s < 60) return 'только что';
  if (s < 3600) return Math.floor(s / 60) + ' мин назад';
  if (s < 86400) return Math.floor(s / 3600) + ' ч назад';
  if (s < 7 * 86400) return Math.floor(s / 86400) + ' дн назад';
  try { return new Date(Number(ts)).toLocaleDateString('ru-RU'); } catch { return ''; }
}

const NOTIF_KIND = {
  copy:   { icon: 'copytrade',   col: 'var(--emerald)', label: 'Копитрейдинг' },
  signal: { icon: 'signal',      col: 'var(--accent)',  label: 'Сигнал' },
  trade:  { icon: 'checkcircle', col: 'var(--green)',   label: 'Сделка' },
  bot:    { icon: 'bot',         col: 'var(--accent)',  label: 'Бот' },
  warn:   { icon: 'warning',     col: 'var(--amber)',   label: 'Внимание' },
  system: { icon: 'bell',        col: 'var(--text-muted)', label: 'Система' },
};

function NotifMenu({ onChange }) {
  const [items, setItems] = useState([]);
  const [unread, setUnread] = useState(0);
  const [expanded, setExpanded] = useState(null);
  const [loading, setLoading] = useState(true);

  const load = async () => {
    try {
      const r = await fetch('/api/me/notifications?limit=50', { credentials: 'include' });
      if (r.ok) { const d = await r.json(); setItems(d.items || []); setUnread(d.unread || 0); }
    } catch {}
    setLoading(false);
  };
  useEffect(() => { load(); }, []);
  const ping = () => { onChange && onChange(); };

  const markRead = async (id) => {
    setItems(its => its.map(i => i.id === id ? { ...i, read: true } : i));
    setUnread(u => Math.max(0, u - 1));
    try { await fetch(`/api/me/notifications/${id}/read`, { method: 'POST', credentials: 'include' }); } catch {}
    ping();
  };
  const del = async (id) => {
    setItems(its => its.filter(i => i.id !== id));
    try { await fetch(`/api/me/notifications/${id}`, { method: 'DELETE', credentials: 'include' }); } catch {}
    ping();
  };
  const markAll = async () => {
    setItems(its => its.map(i => ({ ...i, read: true }))); setUnread(0);
    try { await fetch('/api/me/notifications/read-all', { method: 'POST', credentials: 'include' }); } catch {}
    ping();
  };
  const clearAll = async () => {
    setItems([]); setUnread(0);
    try { await fetch('/api/me/notifications', { method: 'DELETE', credentials: 'include' }); } catch {}
    ping();
  };
  const toggle = (n) => {
    setExpanded(e => e === n.id ? null : n.id);
    if (!n.read) markRead(n.id);
  };
  const goTo = (n) => {
    const u = n.url || '';
    const hash = u.indexOf('#') >= 0 ? u.slice(u.indexOf('#')) : '';
    if (hash) window.location.hash = hash;
    setExpanded(null);
  };

  return (
    <div className="dd" style={{ top: 'calc(var(--header-h) - 6px)', right: 16, width: 384 }} onMouseDown={e => e.stopPropagation()}>
      <div className="row between" style={{ padding: '12px 16px', borderBottom: '1px solid var(--border)' }}>
        <span style={{ fontWeight: 700, fontSize: 14 }}>Уведомления{unread > 0 ? ` · ${unread}` : ''}</span>
        {items.length > 0 && (
          <div className="row" style={{ gap: 12 }}>
            <button className="muted" style={{ fontSize: 12.5 }} onClick={markAll}>Прочитать все</button>
            <button style={{ fontSize: 12.5, color: 'var(--red)', background: 'none', border: 'none', cursor: 'pointer', padding: 0 }} onClick={clearAll}>Очистить</button>
          </div>
        )}
      </div>
      <div style={{ maxHeight: 460, overflowY: 'auto' }}>
        {!loading && items.length === 0 && (
          <div className="dd-item" style={{ cursor: 'default', padding: '20px 16px' }}>
            <Icon name="bell" size={17} style={{ color: 'var(--text-muted)' }} />
            <span className="muted" style={{ fontSize: 13 }}>Уведомлений нет — здесь появятся сделки, сигналы и копитрейдинг.</span>
          </div>
        )}
        {items.map(n => {
          const k = NOTIF_KIND[n.kind] || NOTIF_KIND.system;
          const isOpen = expanded === n.id;
          return (
            <div key={n.id} style={{ borderBottom: '1px solid var(--border)', background: n.read ? 'transparent' : 'var(--accent-soft)' }}>
              <div className="dd-item" style={{ alignItems: 'flex-start', cursor: 'pointer', gap: 10 }} onClick={() => toggle(n)}>
                <span style={{ color: k.col, marginTop: 1, flex: 'none' }}><Icon name={k.icon} size={17} /></span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)', lineHeight: 1.35 }}>{n.title}</div>
                  {n.body && !isOpen && (
                    <div className="muted" style={{ fontSize: 12, marginTop: 2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{n.body}</div>
                  )}
                  {n.body && isOpen && (
                    <div style={{ fontSize: 12.5, marginTop: 5, color: 'var(--text-2)', lineHeight: 1.5, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{n.body}</div>
                  )}
                  <div className="row" style={{ gap: 8, marginTop: 6, alignItems: 'center' }}>
                    <span className="pill pill-soft" style={{ fontSize: 10.5, color: k.col, padding: '1px 7px' }}>{k.label}</span>
                    <span className="muted" style={{ fontSize: 11 }}>{fmtAgo(n.created_at)}</span>
                    {!n.read && <span className="dot" style={{ background: 'var(--accent)' }} />}
                  </div>
                  {isOpen && (
                    <div className="row" style={{ gap: 8, marginTop: 11, flexWrap: 'wrap' }}>
                      {n.url && n.url !== '/' && (
                        <button className="btn btn-ghost btn-sm" style={{ fontSize: 11.5, height: 28 }} onClick={e => { e.stopPropagation(); goTo(n); }}>Перейти</button>
                      )}
                      {!n.read && (
                        <button className="btn btn-ghost btn-sm" style={{ fontSize: 11.5, height: 28 }} onClick={e => { e.stopPropagation(); markRead(n.id); }}>Прочитать</button>
                      )}
                      <button className="btn btn-ghost btn-sm" style={{ fontSize: 11.5, height: 28, color: 'var(--red)' }} onClick={e => { e.stopPropagation(); del(n.id); }}>Удалить</button>
                    </div>
                  )}
                </div>
                <button className="btn btn-ghost btn-icon btn-sm" style={{ flex: 'none' }} title="Удалить"
                  onClick={e => { e.stopPropagation(); del(n.id); }}>
                  <Icon name="x" size={13} style={{ color: 'var(--text-muted)' }} />
                </button>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function Header({ title, theme, setTheme, nav, user, onLogout }) {
  const [notifOpen, setNotifOpen] = useState(false);
  const [userOpen, setUserOpen] = useState(false);
  const [exOpen, setExOpen] = useState(false);
  const [exBusy, setExBusy] = useState(false);
  const [bal] = useLiveData(() => API().loadBalance(), null, [], 30000);
  const [posForBadge] = useLiveData(() => API().loadPositions(), null, [], 60000);
  const [meSettings] = useLiveData(
    async () => { const r = await fetch('/api/me/settings', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, []
  );
  const [credsRaw] = useLiveData(
    async () => { const r = await fetch('/api/me/exchange-creds', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, []
  );
  const toast = useToast();
  const _firstConn = ((credsRaw || []).map(c => (c.exchange || '').toLowerCase()).filter(Boolean))[0] || 'weex';
  const activeEx = (meSettings?.active_exchange || _firstConn).toLowerCase();
  const connectedExs = useMemo(() => {
    const s = new Set((credsRaw || []).map(c => (c.exchange || '').toLowerCase()));
    s.add(activeEx);
    return ['weex', 'okx', 'binance', 'bybit'].filter(e => s.has(e));
  }, [credsRaw, activeEx]);
  const switchExchange = async (id) => {
    if (id === activeEx || exBusy) { setExOpen(false); return; }
    setExBusy(true);
    try {
      await API().setActiveAccount(id, 'default');
      toast(`Активная биржа: ${id.toUpperCase()}`, 'success');
      setTimeout(() => window.location.reload(), 400);
    } catch (e) {
      toast('Не удалось переключить: ' + (e.message || e), 'error');
      setExBusy(false);
    }
  };
  const ex = { name: activeEx.toUpperCase(), status: 'online', balance: bal?.equity || 0 };
  const [notifMeta, , reloadNotif] = useLiveData(
    async () => { const r = await fetch('/api/me/notifications?limit=1', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, [], 20000
  );
  const unread = notifMeta?.unread || 0; // badge = непрочитанные уведомления
  const displayName = user?.name || (user?.email ? user.email.split('@')[0] : 'Трейдер');
  const displayEmail = user?.email || '';
  const displayPlan = user?.plan_tier || 'free';
  useEffect(() => {
    const close = () => { setNotifOpen(false); setUserOpen(false); setExOpen(false); };
    if (notifOpen || userOpen || exOpen) { window.addEventListener('mousedown', close); return () => window.removeEventListener('mousedown', close); }
  }, [notifOpen, userOpen, exOpen]);
  return (
    <header style={{ height: 'var(--header-h)', flex: 'none', borderBottom: '1px solid var(--border)', background: 'var(--bg-1)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 20px', position: 'relative', zIndex: 50 }}>
      <div className="row" style={{ gap: 14 }}>
        <h1 style={{ margin: 0, fontSize: 17, fontWeight: 700 }}>{title}</h1>
      </div>
      <div className="row" style={{ gap: 12 }}>
        {/* Быстрый переключатель бирж */}
        <button className="pill pill-soft" style={{ height: 32, padding: '0 12px', cursor: 'pointer', border: '1px solid var(--border-strong)' }}
          onMouseDown={e => { e.stopPropagation(); setExOpen(o => !o); setNotifOpen(false); setUserOpen(false); }}>
          <span className="dot" style={{ background: 'var(--green)' }} />
          <span className="mono" style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--text)' }}>{exBusy ? '…' : ex.name}</span>
          <Icon name="chevdown" size={13} style={{ color: 'var(--text-muted)' }} />
        </button>
        {exOpen && (
          <div className="dd" style={{ top: 'calc(var(--header-h) - 6px)', right: 280, width: 200 }} onMouseDown={e => e.stopPropagation()}>
            <div style={{ padding: '10px 14px 6px', fontSize: 11, fontWeight: 700, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--text-muted)' }}>Активная биржа</div>
            {['weex', 'okx', 'binance', 'bybit'].map(id => {
              const isConn = connectedExs.includes(id);
              const isActive = id === activeEx;
              return (
                <div key={id} className="dd-item" style={{ opacity: isConn ? 1 : 0.45, cursor: isConn ? 'pointer' : 'not-allowed' }}
                  onClick={() => isConn && switchExchange(id)}>
                  <span className="dot" style={{ background: isActive ? 'var(--green)' : isConn ? 'var(--text-muted)' : 'var(--border-strong)' }} />
                  <span className="mono" style={{ flex: 1, fontWeight: isActive ? 700 : 500, color: isActive ? 'var(--text)' : undefined }}>{id.toUpperCase()}</span>
                  {isActive && <Icon name="check" size={14} style={{ color: 'var(--green)' }} />}
                  {!isConn && <span className="muted" style={{ fontSize: 10 }}>нет ключа</span>}
                </div>
              );
            })}
          </div>
        )}
        <div className="row mono" style={{ gap: 6, fontSize: 13, fontWeight: 600, padding: '0 4px' }}>
          <Icon name="wallet" size={15} style={{ color: 'var(--text-muted)' }} />
          {fmt.usdt(ex.balance)} <span className="muted" style={{ fontWeight: 400 }}>USDT</span>
        </div>
        <button className="btn btn-ghost btn-icon btn-sm" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} title="Тема">
          <Icon name={theme === 'dark' ? 'sun' : 'moon'} size={17} />
        </button>
        <button className="btn btn-ghost btn-icon btn-sm" style={{ position: 'relative' }} onMouseDown={e => { e.stopPropagation(); setNotifOpen(o => !o); setUserOpen(false); }}>
          <Icon name="bell" size={17} />
          {unread > 0 && (
            <span style={{ position: 'absolute', top: -2, right: -2, minWidth: 15, height: 15, padding: '0 3px', borderRadius: 99, background: 'var(--red)', border: '1.5px solid var(--bg-1)', color: '#fff', fontSize: 9.5, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', lineHeight: 1 }}>
              {unread > 99 ? '99+' : unread}
            </span>
          )}
        </button>
        <button onMouseDown={e => { e.stopPropagation(); setUserOpen(o => !o); setNotifOpen(false); }} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <Avatar name={displayName} hue={210} size={32} />
        </button>
        {notifOpen && <NotifMenu onChange={reloadNotif} />}
        {userOpen && (
          <div className="dd" style={{ top: 'calc(var(--header-h) - 6px)', right: 16, width: 240 }} onMouseDown={e => e.stopPropagation()}>
            <div style={{ padding: '14px 16px', borderBottom: '1px solid var(--border)' }}>
              <div style={{ fontWeight: 700, fontSize: 14 }}>{displayName}</div>
              <div className="muted" style={{ fontSize: 12.5 }}>{displayEmail}</div>
              <span className="badge badge-accent" style={{ marginTop: 8 }}>{displayPlan}</span>
            </div>
            <div className="dd-item" onClick={() => { nav('settings'); setUserOpen(false); }}><Icon name="settings" size={16} />Настройки</div>
            <div className="dd-item" onClick={() => { nav('exchange'); setUserOpen(false); }}><Icon name="exchange" size={16} />Биржа</div>
            <div className="divider" />
            <div className="dd-item" style={{ color: 'var(--red)' }} onClick={() => { setUserOpen(false); onLogout && onLogout(); }}>
              <Icon name="logout" size={16} />Выйти
            </div>
          </div>
        )}
      </div>
    </header>
  );
}

/* ===================== DASHBOARD ======================================== */
function VerifyEmailBanner({ user }) {
  const [shown, setShown] = useState(!!user && user.email_verified === false);
  const [busy, setBusy] = useState(false);
  const toast = useToast();
  if (!shown) return null;
  const resend = async () => {
    setBusy(true);
    try {
      const r = await fetch('/api/auth/resend-verify', { method: 'POST', credentials: 'include' });
      const j = await r.json().catch(() => ({}));
      if (j.already_verified) { toast('Почта уже подтверждена', 'success'); setShown(false); }
      else if (j.sent === false) { toast('Email-сервис не настроен — обратитесь в поддержку', 'error'); }
      else { toast('Письмо отправлено · проверь почту', 'success'); }
    } catch (e) { toast('Не удалось отправить письмо', 'error'); }
    setBusy(false);
  };
  return (
    <div className="card card-pad" style={{ display: 'flex', alignItems: 'center', gap: 14, background: 'var(--amber-soft)', border: '1px solid var(--amber)' }}>
      <Icon name="mail" size={20} style={{ color: 'var(--amber)', flex: 'none' }} />
      <div style={{ flex: 1 }}>
        <div style={{ fontWeight: 600, fontSize: 14 }}>Подтвердите email</div>
        <div className="muted" style={{ fontSize: 12.5 }}>Без подтверждения часть функций ограничена.</div>
      </div>
      <button className="btn btn-ghost btn-sm" disabled={busy} onClick={resend}>{busy ? 'Отправка…' : 'Отправить письмо'}</button>
      <button className="btn btn-ghost btn-icon btn-sm" onClick={() => setShown(false)}><Icon name="x" size={14} /></button>
    </div>
  );
}

function PositionRow({ p, onClick }) {
  return (
    <tr className="clickable" onClick={onClick}>
      <td>
        <div className="row" style={{ gap: 10 }}>
          <span className={cx('badge', p.side === 'long' ? 'badge-long' : 'badge-short')}>{p.side === 'long' ? 'LONG' : 'SHORT'}</span>
          <span className="strong mono">{p.symbol}</span>
          <span className="muted mono" style={{ fontSize: 11.5 }}>×{p.lev}</span>
        </div>
      </td>
      <td className="num mono">{fmt.usdt(p.size)}</td>
      <td className="num mono">{p.entry}</td>
      <td className="num mono">{p.mark}</td>
      <td className="num mono" style={{ color: 'var(--amber)' }}>{p.liq}</td>
      <td className="num">
        <div className={cx('mono', pn(p.pnl))} style={{ fontWeight: 600 }}>{fmt.sign(p.pnl)}{fmt.usdt(p.pnl)}</div>
        <div className={cx('mono', pn(p.pnl))} style={{ fontSize: 11.5 }}>{fmt.pct(p.pnlPct)}</div>
      </td>
      <td><span className="pill pill-soft">{p.source}</span></td>
    </tr>
  );
}

function Dashboard({ nav, user }) {
  const [tab, setTab] = useState('futures');
  const [selPos, setSelPos] = useState(null);
  const toast = useToast();
  // LIVE: balance, positions, bots, equity curve, channels
  const [bal] = useLiveData(() => API().loadBalance(), null, [], 60000);
  const [posRaw, , refreshPos] = useLiveData(() => API().loadPositions(), null, [], 30000);
  const [closedIds, setClosedIds] = useState(() => new Set());
  const [botsRaw] = useLiveData(() => API().listLocalBots(), null, [], 60000);
  const [eqRaw] = useLiveData(() => API().loadEquityHistory(30), null, []);
  const [chansRaw] = useLiveData(() => API().listChannels(), null, []);
  const [dashSettings] = useLiveData(
    async () => { const r = await fetch('/api/me/settings', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, []
  );
  const dashEx = (dashSettings?.active_exchange || 'weex').toLowerCase();
  const [ordersRaw] = useLiveData(
    async () => {
      const r = await fetch(`/api/ex/${dashEx}/open-orders`, { credentials: 'include' });
      if (!r.ok) return [];
      const j = await r.json();
      return Array.isArray(j) ? j : (j.orders || j.data || []);
    },
    null, [dashEx], 30000
  );
  const dashOrders = (ordersRaw || []).map((o, i) => ({
    id: o.orderId || o.order_id || o.ordId || ('o' + i),
    symbol: (o.symbol || o.instId || '').replace(/[-_]/g, '').replace('SWAP', ''),
    side: /buy|long/i.test(String(o.side || o.posSide || '')) ? 'long' : 'short',
    type: String(o.orderType || o.ordType || o.type || 'LIMIT').toUpperCase(),
    price: parseFloat(o.price || o.px || 0),
    qty: parseFloat(o.size || o.qty || o.sz || 0),
  }));
  // ВАЖНО: при null/ошибке API показываем РЕАЛЬНОЕ пустое состояние, а НЕ
  // mock-данные (HDB). Иначе залогиненный юзер видел бы фейковые позиции/боты/
  // equity на каждой загрузке — недопустимо для финансового продукта.
  const ex = bal ? {
    name: 'WEEX', status: 'online',
    balance: bal.equity || 0, available: bal.available || 0, equity: bal.equity || 0,
    pnlDay: bal.dayPnl || 0, pnlDayPct: bal.dayPnlPct || 0,
    tradesDay: bal.dayTrades || 0, wins: bal.dayWins || 0, losses: bal.dayLosses || 0,
    winRate: (bal.dayWins + bal.dayLosses) > 0 ? Math.round(bal.dayWins / (bal.dayWins + bal.dayLosses) * 100) : 0,
  } : { name: 'WEEX', status: 'offline', balance: 0, available: 0, equity: 0,
        pnlDay: 0, pnlDayPct: 0, tradesDay: 0, wins: 0, losses: 0, winRate: 0 };
  const positions = (posRaw || []).map(mapPosition).filter(p => !closedIds.has(p.id));
  // Очищаем «скрытые после закрытия» id, когда биржа перестала их отдавать,
  // чтобы повторно открытая та же монета снова показывалась.
  useEffect(() => {
    if (!posRaw) return;
    setClosedIds(prev => {
      if (!prev.size) return prev;
      const present = new Set((posRaw || []).map(mapPosition).map(p => p.id));
      const next = new Set([...prev].filter(id => present.has(id)));
      return next.size === prev.size ? prev : next;
    });
  }, [posRaw]);
  // После закрытия: мгновенно прячем (если 100%) + рефреш с повтором (биржа закрывает не сразу).
  const handlePosClosed = (pct) => {
    if (pct === 100 && selPos) { const id = selPos.id; setClosedIds(s => new Set(s).add(id)); }
    refreshPos();
    setTimeout(refreshPos, 1500);
    setTimeout(refreshPos, 4000);
  };
  const allLocalBots = (botsRaw || []).map(mapLocalBot);
  const equity = useMemo(() => {
    if (eqRaw && eqRaw.length) return eqRaw.map(p => ({ t: new Date(p.date || p.t || Date.now()).getTime(), v: p.equity || p.value || p.v || 0 }));
    return [];
  }, [eqRaw]);
  // Реальный дневной PnL %: текущий эквити vs точка ~24ч назад (из кривой капитала)
  const dayPct = useMemo(() => {
    if (!equity || equity.length < 2) return null;
    const last = equity[equity.length - 1].v;
    const dayAgo = Date.now() - 24 * 3600 * 1000;
    const ref = equity.find(p => p.t >= dayAgo) || equity[0];
    if (!ref || !ref.v) return null;
    return ((last - ref.v) / ref.v) * 100;
  }, [equity]);
  const channels = (chansRaw || []).map(mapChannel);
  const [copySubsResp] = useLiveData(
    async () => { const r = await fetch('/api/me/copytrade/subscriptions', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, [], 60000
  );
  const copySubs = (copySubsResp?.subscriptions || []).filter(s => s.status === 'active');
  const copyPnl = copySubs.reduce((s, c) => s + (c.realized_pnl || 0), 0);
  const copyAlloc = copySubs.reduce((s, c) => s + (c.allocation_usdt || 0), 0);
  const allocByChannel = channels.filter(c => c.on).map(c => ({ label: c.name, value: Math.abs(c.pnl30) + 200, color: c.accent }));
  const [tpStats] = useLiveData(() => API().loadTpStats(7), null, []);
  const [slipStats] = useLiveData(() => API().loadSlippageStats(7), null, []);
  const winRate7d = ex.winRate || 0;
  // TP/SL hit-rate по реальной стате (TpStats возвращает {tp1_hit_pct, tp2_hit_pct, tp3_hit_pct, rr_avg})
  const tp1Pct = tpStats?.tp1_hit_pct != null ? Math.round(tpStats.tp1_hit_pct) : null;
  const tp2Pct = tpStats?.tp2_hit_pct != null ? Math.round(tpStats.tp2_hit_pct) : null;
  const tp3Pct = tpStats?.tp3_hit_pct != null ? Math.round(tpStats.tp3_hit_pct) : null;
  const rrAvg = tpStats?.rr_avg != null ? '1:' + Number(tpStats.rr_avg).toFixed(1) : '—';
  // avg slippage на сделку в USDT (раньше считали псевдо-bps — выходило 4294 bps, бред)
  const slipAvg = slipStats && slipStats.samples > 0 ? (slipStats.total_slip_usdt / slipStats.samples) : null;

  return (
    <div className="fade-in" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20 }}>
      <VerifyEmailBanner user={user} />

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }}>
        <div className="card card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          <span className="label-cap">Баланс</span>
          <div className="row" style={{ gap: 8, alignItems: 'baseline' }}>
            <span className="mono" style={{ fontSize: 30, fontWeight: 700, letterSpacing: '-0.02em' }}>{fmt.usdt(ex.balance)}</span>
            <span className="muted" style={{ fontSize: 13 }}>USDT</span>
          </div>
          <div className="muted" style={{ fontSize: 12.5 }}>Доступно: <span className="mono" style={{ color: 'var(--text-2)' }}>{fmt.usdt(ex.available)}</span></div>
          <div style={{ marginTop: 6 }}><Sparkline data={equity.slice(-30)} w={240} h={34} color="var(--accent)" /></div>
        </div>
        <div className="card card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          <div className="row between"><span className="label-cap">Прибыль за сутки</span><span className="muted mono" style={{ fontSize: 12 }}>{ex.tradesDay} сделок</span></div>
          <div className="row" style={{ gap: 8, alignItems: 'baseline' }}>
            <span className={cx('mono', pn(ex.pnlDay))} style={{ fontSize: 30, fontWeight: 700 }}>{fmt.sign(ex.pnlDay)}{fmt.usdt(ex.pnlDay)}</span>
            <span className={cx('mono', pn(ex.pnlDay))} style={{ fontSize: 14, fontWeight: 600 }}>({fmt.pct(ex.pnlDayPct)})</span>
          </div>
          <div style={{ marginTop: 8 }}><WinBar wins={ex.wins} losses={ex.losses} /></div>
          <div className="row" style={{ gap: 12, marginTop: 8, fontSize: 12 }}>
            <span className="mono"><span className="pos">{ex.wins}W</span> <span className="neg">{ex.losses}L</span></span>
            <span className="muted">win rate <span className="mono" style={{ color: 'var(--text-2)' }}>{ex.winRate}%</span></span>
          </div>
        </div>
        <div className="card card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          <div className="row between"><span className="label-cap">Активные боты</span><span style={{ color: 'var(--emerald)' }}><Icon name="bot" size={16} /></span></div>
          <div className="row" style={{ gap: 8, alignItems: 'baseline' }}>
            <span className="mono" style={{ fontSize: 30, fontWeight: 700 }}>{allLocalBots.filter(b => b.status === 'running').length}</span>
            <span className="muted" style={{ fontSize: 13 }}>/ {allLocalBots.length}</span>
          </div>
          <div className="muted" style={{ fontSize: 12.5 }}>
            {allLocalBots.length
              ? `инвестировано ${fmt.usdt(allLocalBots.reduce((s, b) => s + (b.invest || 0), 0), 0)} USDT`
              : 'создай первого бота'}
          </div>
          <button className="btn btn-ghost btn-sm" style={{ marginTop: 'auto', alignSelf: 'flex-start' }} onClick={() => nav('bots')}>Управление <Icon name="chevright" size={14} /></button>
        </div>
        <div className="card card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          <div className="row between"><span className="label-cap">Копи-трейдинг</span><span style={{ color: 'var(--accent)' }}><Icon name="copytrade" size={16} /></span></div>
          <div className="row" style={{ gap: 8, alignItems: 'baseline' }}>
            <span className={cx('mono', pn(copyPnl))} style={{ fontSize: 30, fontWeight: 700 }}>{fmt.sign(copyPnl)}{fmt.usdt(copyPnl)}</span>
          </div>
          <div className="muted" style={{ fontSize: 12.5 }}>{copySubs.length} {copySubs.length === 1 ? 'мастер' : 'мастеров'} · аллокация {fmt.usdt(copyAlloc, 0)}</div>
          <button className="btn btn-ghost btn-sm" style={{ marginTop: 'auto', alignSelf: 'flex-start' }} onClick={() => nav('copytrade')}>Открыть <Icon name="chevright" size={14} /></button>
        </div>
      </div>

      {/* Analytics top bar (live) */}
      <div className="card card-pad" style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: 14 }}>
        <div><div className="label-cap" style={{ fontSize: 10 }}>Win Rate · 7D</div><div className="mono pos" style={{ fontSize: 18, fontWeight: 700, marginTop: 4 }}>{winRate7d}%</div></div>
        <div><div className="label-cap" style={{ fontSize: 10 }}>R:R средний</div><div className="mono" style={{ fontSize: 18, fontWeight: 700, marginTop: 4 }}>{rrAvg}</div></div>
        <div><div className="label-cap" style={{ fontSize: 10 }}>TP1 hit</div><div className="mono pos" style={{ fontSize: 18, fontWeight: 700, marginTop: 4 }}>{tp1Pct != null ? tp1Pct + '%' : '—'}</div></div>
        <div><div className="label-cap" style={{ fontSize: 10 }}>TP2 hit</div><div className="mono" style={{ fontSize: 18, fontWeight: 700, marginTop: 4, color: 'var(--accent)' }}>{tp2Pct != null ? tp2Pct + '%' : '—'}</div></div>
        <div><div className="label-cap" style={{ fontSize: 10 }}>TP3 hit</div><div className="mono" style={{ fontSize: 18, fontWeight: 700, marginTop: 4, color: 'var(--accent)' }}>{tp3Pct != null ? tp3Pct + '%' : '—'}</div></div>
        <div><div className="label-cap" style={{ fontSize: 10 }}>Slippage / сделка</div><div className="mono" style={{ fontSize: 18, fontWeight: 700, marginTop: 4 }}>{slipAvg != null ? fmt.usdt(slipAvg) + ' $' : '—'}</div></div>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 320px', gap: 16 }}>
        <div className="card card-pad">
          <div className="row between" style={{ marginBottom: 8 }}>
            <div>
              <div className="label-cap">Кривая капитала</div>
              <div className="row" style={{ gap: 8, alignItems: 'baseline', marginTop: 4 }}>
                <span className="mono" style={{ fontSize: 22, fontWeight: 700 }}>{fmt.usdt(ex.equity)}</span>
                {dayPct != null && <span className={cx('mono', dayPct >= 0 ? 'pos' : 'neg')} style={{ fontSize: 13 }}>{fmt.pct(dayPct)} сегодня</span>}
              </div>
            </div>
            <div className="seg">
              {['24ч', '7д', '30д', 'Всё'].map((t, i) => <button key={t} className={i === 2 ? 'on' : ''}>{t}</button>)}
            </div>
          </div>
          <AreaChart data={equity} height={250} color="var(--accent)" />
        </div>
        <div className="card card-pad" style={{ display: 'flex', flexDirection: 'column' }}>
          <div className="label-cap" style={{ marginBottom: 14 }}>P&L по каналам · 30д</div>
          <div className="row" style={{ justifyContent: 'center', marginBottom: 16 }}>
            <Donut segments={allocByChannel} size={140} thickness={18} center={
              <>
                <span className="mono pos" style={{ fontSize: 18, fontWeight: 700 }}>+{fmt.compact(channels.reduce((s, c) => s + (c.on ? c.pnl30 : 0), 0))}</span>
                <span className="muted" style={{ fontSize: 11 }}>USDT</span>
              </>
            } />
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {channels.filter(c => c.on).map(c => (
              <div key={c.id} className="row between" style={{ fontSize: 12.5 }}>
                <div className="row" style={{ gap: 8 }}><span className="dot" style={{ background: c.accent, width: 9, height: 9 }} /><span>{c.name}</span></div>
                <span className={cx('mono', pn(c.pnl30))}>{fmt.sign(c.pnl30)}{fmt.compact(c.pnl30)}</span>
              </div>
            ))}
          </div>
        </div>
      </div>

      <div className="card">
        <div className="card-pad" style={{ paddingBottom: 0 }}>
          <div className="row between">
            <SectionHead title="Открытые позиции" count={positions.length} />
            <div className="tabs" style={{ border: 'none' }}>
              {[['futures', 'Фьючерсы'], ['spot', 'Спот'], ['bots', 'Боты']].map(([k, l]) => (
                <button key={k} className={tab === k ? 'on' : ''} onClick={() => setTab(k)}>{l}</button>
              ))}
            </div>
          </div>
        </div>
        {tab === 'futures' ? (
          positions.length > 0 ? (
            <div style={{ overflowX: 'auto' }}>
              <table className="tbl">
                <thead><tr>
                  <th>Позиция</th><th className="num">Размер</th><th className="num">Вход</th><th className="num">Текущ.</th><th className="num">Ликвид.</th><th className="num">PnL</th><th>Источник</th>
                </tr></thead>
                <tbody>{positions.map(p => <PositionRow key={p.id} p={p} onClick={() => setSelPos(p)} />)}</tbody>
              </table>
            </div>
          ) : <Empty icon="layers" title="Нет открытых позиций" text="Бот откроет сделку при сигнале, либо открой вручную на бирже." />
        ) : tab === 'spot' ? (
          <Empty icon="spot" title="Нет спот-позиций" text="Переключись на вкладку «Спот» в меню." />
        ) : (
          <Empty icon="layers" title="Нет открытых позиций" text="Бот откроет сделку при следующем сигнале." />
        )}
      </div>

      {/* Активные ордера (перенесено из вкладки Биржа) */}
      <div className="card">
        <div className="card-pad" style={{ paddingBottom: 0 }}>
          <SectionHead title="Активные ордера" count={dashOrders.length} />
        </div>
        {dashOrders.length ? (
          <table className="tbl">
            <thead><tr><th>Символ</th><th>Сторона</th><th>Тип</th><th className="num">Цена</th><th className="num">Кол-во</th><th>Статус</th></tr></thead>
            <tbody>{dashOrders.map(o => (
              <tr key={o.id}>
                <td className="strong mono">{o.symbol}</td>
                <td><span className={cx('badge', o.side === 'long' ? 'badge-long' : 'badge-short')}>{o.side.toUpperCase()}</span></td>
                <td><span className="pill pill-soft">{o.type}</span></td>
                <td className="num mono">{o.price || '—'}</td>
                <td className="num mono">{o.qty || '—'}</td>
                <td><StatusPill status="pending" /></td>
              </tr>
            ))}</tbody>
          </table>
        ) : <Empty icon="journal" title="Нет активных ордеров" text="Лимитные/стоп-ордера появятся здесь." />}
      </div>

      {selPos && <PositionModal p={selPos} onClose={() => setSelPos(null)} onClosed={handlePosClosed} />}
    </div>
  );
}

/* ---- Модал позиции с частичным закрытием (25/50/75/100%) ---- */
function PositionModal({ p, onClose, onClosed }) {
  const [closePct, setClosePct] = useState(100);
  const [busy, setBusy] = useState(false);
  const toast = useToast();
  const qtyFull = p.qty || 0;
  const qtyToClose = qtyFull * closePct / 100;
  const doClose = async () => {
    setBusy(true);
    try {
      // sig: closePositionAction(sym, side, qty, type='market', limitPrice?, exchange?)
      await API().closePositionAction(p.symbol, p.side, qtyToClose || 0, 'market');
      toast(closePct === 100 ? 'Позиция закрыта по рынку' : `Закрыто ${closePct}% позиции`, 'success');
      onClosed && onClosed(closePct);
      onClose();
    } catch (e) { toast('Не удалось закрыть: ' + (e.message || e), 'error'); }
    setBusy(false);
  };
  return (
    <Modal title={`${p.symbol} · ${p.side === 'long' ? 'LONG' : 'SHORT'} ×${p.lev}`} sub={`Источник: ${p.source || '—'}`} onClose={onClose}
      footer={<>
        <button className="btn btn-ghost" disabled={busy} onClick={onClose}>Закрыть окно</button>
        <button className="btn btn-danger" disabled={busy} onClick={doClose}>
          {busy ? 'Закрываем…' : (closePct === 100 ? 'Закрыть позицию' : `Закрыть ${closePct}%`)}
        </button>
      </>}>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, marginBottom: 16 }}>
        {[['Нереализованный PnL', <span className={pn(p.pnl)}>{fmt.sign(p.pnl)}{fmt.usdt(p.pnl)} ({fmt.pct(p.pnlPct)})</span>],
          ['Размер позиции', `${qtyFull ? qtyFull.toFixed(4) + ' · ' : ''}${fmt.usdt(p.size)} USDT`],
          ['Цена входа', p.entry], ['Текущая цена', p.mark],
          ['Цена ликвидации', <span style={{ color: 'var(--amber)' }}>{p.liq || '—'}</span>], ['Маржа', fmt.usdt(p.margin) + ' USDT']].map(([l, v], i) => (
          <div key={i} className="card" style={{ padding: '12px 14px', background: 'var(--bg-2)' }}>
            <div className="label-cap" style={{ fontSize: 10.5 }}>{l}</div>
            <div className="mono" style={{ fontSize: 16, fontWeight: 600, marginTop: 4 }}>{v}</div>
          </div>
        ))}
      </div>
      {/* Частичное закрытие — как на мобилке и в OG */}
      <div className="label-cap" style={{ marginBottom: 8 }}>Сколько закрыть</div>
      <div className="seg" style={{ width: '100%', marginBottom: 8 }}>
        {[25, 50, 75, 100].map(v => (
          <button key={v} style={{ flex: 1 }} className={closePct === v ? 'on' : ''} onClick={() => setClosePct(v)}>{v}%</button>
        ))}
      </div>
      <div className="row between" style={{ fontSize: 12.5 }}>
        <span className="muted">Будет закрыто</span>
        <span className="mono" style={{ fontWeight: 600 }}>
          {qtyToClose ? qtyToClose.toFixed(4) : '—'} ({fmt.usdt(p.size * closePct / 100)} USDT) · market
        </span>
      </div>
      {closePct < 100 && p.pnl !== 0 && (
        <div className="muted" style={{ fontSize: 11.5, marginTop: 8 }}>
          Зафиксируется ≈ <span className={cx('mono', pn(p.pnl))}>{fmt.sign(p.pnl)}{fmt.usdt(p.pnl * closePct / 100)} USDT</span> PnL, остальное останется в позиции.
        </div>
      )}
    </Modal>
  );
}

/* ===================== BOTS (rewritten) ================================= */
const BOT_TYPES = [
  { id: 'grid', label: 'Grid (фьючерсы)', icon: 'grid', desc: 'Сетка лимиток в диапазоне — зарабатывает на волатильности' },
  { id: 'dca', label: 'Smart-DCA', icon: 'scale', desc: 'Мартингейл-усреднение с safety-ордерами (OKX-style)' },
  { id: 'recurring', label: 'Recurring Basket', icon: 'refresh', desc: 'Периодическая закупка корзины монет' },
];

function BotCard({ b, onOpen, onToggle }) {
  const typeLabel = BOT_TYPES.find(t => t.id === b.type)?.label || b.type;
  return (
    <div className="card card-pad fade-in" style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <div className="row between">
        <div className="row" style={{ gap: 12 }}>
          <Avatar name={b.symbol || b.name} hue={(String(b.id)).charCodeAt(0) * 7 % 360} size={40} square />
          <div>
            <div style={{ fontWeight: 700, fontSize: 14.5 }}>{b.symbol || b.name}</div>
            <div className="muted" style={{ fontSize: 12, marginTop: 2 }}>{typeLabel} · {b.exchange}</div>
          </div>
        </div>
        <StatusPill status={b.status} />
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
        <div><div className="label-cap" style={{ fontSize: 10 }}>Инвест</div><div className="mono" style={{ fontSize: 15, fontWeight: 700, marginTop: 3 }}>{fmt.usdt(b.invest)}</div></div>
        <div><div className="label-cap" style={{ fontSize: 10 }}>P&L</div><div className={cx('mono', pn(b.pnl))} style={{ fontSize: 15, fontWeight: 700, marginTop: 3 }}>{fmt.sign(b.pnl)}{fmt.usdt(b.pnl)}</div></div>
        <div><div className="label-cap" style={{ fontSize: 10 }}>P&L %</div><div className={cx('mono', pn(b.pnl))} style={{ fontSize: 15, fontWeight: 700, marginTop: 3 }}>{fmt.pct(b.pnlPct)}</div></div>
      </div>
      <div className="row" style={{ gap: 8, fontSize: 11.5 }}>
        {b.lev > 1 && <span className="pill pill-soft mono">×{b.lev}</span>}
        <span className="pill pill-soft">{b.days} дн</span>
        {b.source && <span className="pill pill-soft" style={{ color: 'var(--accent)' }}>{b.source}</span>}
      </div>
      <div className="divider" />
      <div className="row between">
        <button className="btn btn-ghost btn-sm" onClick={() => onOpen(b)}>Подробнее</button>
        <div className="row" style={{ gap: 8 }}>
          <button className="btn btn-ghost btn-icon btn-sm" title={b.status === 'running' ? 'Стоп' : 'Запуск'} onClick={() => onToggle(b)}>
            <Icon name={b.status === 'running' ? 'pause' : 'play'} size={15} />
          </button>
        </div>
      </div>
    </div>
  );
}

/* ---- helper: подписанное числовое поле с ручным вводом (как на мобилке) ---- */
// Цветной кружок монеты (детерминированный hue от тикера) — как Coin на мобилке
const COIN_COLORS = {
  BTC: '#f7931a', ETH: '#627eea', SOL: '#9945ff', BNB: '#f0b90b', TON: '#0098ea',
  XRP: '#23292f', DOGE: '#c2a633', ADA: '#0033ad', AVAX: '#e84142', LINK: '#2a5ada',
  ARB: '#28a0f0', OP: '#ff0420', SUI: '#4da2ff', SEI: '#9e1f19', PEPE: '#3d8b40',
  WIF: '#c8a06a', BCH: '#0ac18e', LTC: '#345d9d', DOT: '#e6007a', MATIC: '#8247e5',
};
function CoinDot({ sym, size = 26 }) {
  const base = String(sym || '?').replace(/USDT$/i, '').replace(/^\d+/, '') || sym || '?';
  const col = COIN_COLORS[base.toUpperCase()];
  let hue = 0; for (let i = 0; i < base.length; i++) hue = (hue * 31 + base.charCodeAt(i)) % 360;
  const bg = col || `hsl(${hue} 62% 48%)`;
  return (
    <span style={{
      width: size, height: size, flex: 'none', borderRadius: '50%',
      background: bg, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
      color: '#fff', fontWeight: 700, fontSize: size * 0.4, letterSpacing: '-0.02em',
    }}>{base.slice(0, 2).toUpperCase()}</span>
  );
}

// Красивый searchable-дропдаун символов (вместо нативного datalist)
// Популярные монеты — показываем первыми в выпадашке (как просили: биток, эфир,
// соль и т.д.), остальное ниже по алфавиту. Список упорядочен по спросу; правь
// под аудиторию. Учитываем мем-варианты бирж: 1000PEPEUSDT → PEPE.
const POPULAR_BASES = [
  'BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'DOGE', 'TON', 'ADA', 'AVAX', 'LINK',
  'TRX', 'DOT', 'SUI', 'NEAR', 'LTC', 'BCH', 'APT', 'ARB', 'OP', 'PEPE',
  'WIF', 'SHIB', 'UNI', 'ATOM', 'INJ', 'SEI', 'TIA', 'FIL', 'AAVE', 'ENA',
  'ORDI', 'BONK', 'FLOKI', 'RNDR', 'FET', 'HBAR', 'XLM', 'ETC', 'ICP', 'IMX',
];
const POPULAR_RANK = POPULAR_BASES.reduce((m, b, i) => { m[b] = i; return m; }, {});
const _symBase = (s) => s.replace(/USDT$|USDC$/, '').replace(/^1000/, '');
const _popRank = (s) => {
  const r = POPULAR_RANK[_symBase(s)];
  return r === undefined ? 999 : r;
};

function SymbolPicker({ value, onChange, symbols, kind = 'fut', placeholder, style }) {
  const [open, setOpen] = useState(false);
  const [q, setQ] = useState('');
  const ref = useRef(null);
  useEffect(() => {
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, []);
  const query = (open ? q : value) || '';
  const filtered = useMemo(() => {
    const Q = query.toUpperCase();
    const list = Q ? symbols.filter(s => s.includes(Q)) : symbols.slice();
    list.sort((a, b) => {
      // При вводе: сперва те, что начинаются с запроса (BT → BTCUSDT раньше 1000BTTUSDT)
      if (Q) {
        const aStarts = a.startsWith(Q) ? 0 : 1;
        const bStarts = b.startsWith(Q) ? 0 : 1;
        if (aStarts !== bStarts) return aStarts - bStarts;
      }
      // Затем популярные монеты, затем по алфавиту
      const ra = _popRank(a), rb = _popRank(b);
      if (ra !== rb) return ra - rb;
      return a.localeCompare(b);
    });
    return list.slice(0, 200);
  }, [query, symbols]);
  const pick = (s) => { onChange(s); setQ(''); setOpen(false); };

  return (
    <div ref={ref} style={{ position: 'relative', ...style }}>
      <div style={{ position: 'relative' }}>
        <input className="inp mono" value={open ? q : value}
          onFocus={() => { setOpen(true); setQ(''); }}
          onChange={e => { setQ(e.target.value.toUpperCase()); setOpen(true); onChange(e.target.value.toUpperCase()); }}
          placeholder={placeholder || 'Поиск монеты…'}
          style={{ paddingRight: 34 }} />
        <Icon name="chevdown" size={16} style={{ position: 'absolute', right: 11, top: 11, color: 'var(--text-muted)', pointerEvents: 'none', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .15s' }} />
      </div>
      {open && (
        <div className="card" style={{
          position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0, zIndex: 60,
          maxHeight: 300, overflowY: 'auto', padding: 4, background: 'var(--bg-2)',
          border: '1px solid var(--border-strong)', boxShadow: 'var(--shadow)',
        }}>
          {!symbols.length && <div className="muted" style={{ padding: 12, fontSize: 12.5, textAlign: 'center' }}>Загружаем инструменты…</div>}
          {symbols.length > 0 && filtered.length === 0 && <div className="muted" style={{ padding: 12, fontSize: 12.5, textAlign: 'center' }}>Ничего не найдено</div>}
          {filtered.map(s => {
            const base = s.replace(/USDT$/, '');
            const active = s === value;
            return (
              <button key={s} onMouseDown={() => pick(s)}
                style={{
                  width: '100%', display: 'flex', alignItems: 'center', gap: 10,
                  padding: '8px 10px', border: 0, borderRadius: 8, cursor: 'pointer',
                  background: active ? 'var(--accent-soft)' : 'transparent', textAlign: 'left',
                  color: 'var(--text)', transition: 'background .1s',
                }}
                onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'var(--bg-hover)'; }}
                onMouseLeave={e => { if (!active) e.currentTarget.style.background = 'transparent'; }}>
                <CoinDot sym={base} size={26} />
                <span style={{ flex: 1, minWidth: 0 }}>
                  <span className="mono" style={{ fontWeight: 600, fontSize: 13.5 }}>{s}</span>
                  <span className="muted" style={{ fontSize: 10.5, marginLeft: 7 }}>{base}/USDT</span>
                </span>
                <span className="badge" style={{ background: kind === 'spot' ? 'var(--accent-soft)' : 'var(--amber-soft)', color: kind === 'spot' ? 'var(--accent)' : 'var(--amber)', fontSize: 9 }}>{kind === 'spot' ? 'SPOT' : 'FUT'}</span>
                {active && <Icon name="check" size={14} style={{ color: 'var(--accent)' }} />}
              </button>
            );
          })}
        </div>
      )}
    </div>
  );
}

function NumField({ label, value, onChange, placeholder, hint, suffix, style }) {
  return (
    <div style={style}>
      <div className="label-cap" style={{ marginBottom: 6 }}>{label}</div>
      <div style={{ position: 'relative' }}>
        <input className="inp mono" type="text" inputMode="decimal" value={value}
          onChange={e => onChange(e.target.value.replace(',', '.'))} placeholder={placeholder} />
        {suffix && <span className="muted mono" style={{ position: 'absolute', right: 12, top: 10, fontSize: 12 }}>{suffix}</span>}
      </div>
      {hint && <div className="muted" style={{ fontSize: 10.5, marginTop: 4 }}>{hint}</div>}
    </div>
  );
}

function CreateBotWizard({ onClose, onCreated }) {
  const [step, setStep] = useState(1);
  const [type, setType] = useState(null);
  const [exchange, setExchange] = useState('weex');
  const [symbol, setSymbol] = useState('');
  const [direction, setDirection] = useState('long');
  const [invest, setInvest] = useState('100');
  const [lev, setLev] = useState('10');
  // grid
  const [gridLow, setGridLow] = useState('');
  const [gridHigh, setGridHigh] = useState('');
  const [gridNum, setGridNum] = useState('20');
  const [gridWindow, setGridWindow] = useState('2');  // активных ордеров на бирже с каждой стороны
  const [gridTp, setGridTp] = useState('');
  const [gridSl, setGridSl] = useState('');
  // Продвинутые grid-фичи (все opt-in)
  const [gxTrailing, setGxTrailing] = useState(false);
  const [gxExpand, setGxExpand] = useState(false);
  const [gxReinvest, setGxReinvest] = useState(false);
  const [gxDynamic, setGxDynamic] = useState(false);
  const [gxNotify, setGxNotify] = useState(false);
  const [gxMaxHours, setGxMaxHours] = useState('');   // стоп по времени, часов
  const [gxTpRoi, setGxTpRoi] = useState('');         // стоп по профиту, % ROI
  // dca (Smart-Martingale, OKX-style)
  const [dcaIm, setDcaIm] = useState('20');
  const [dcaSm, setDcaSm] = useState('20');
  const [dcaPs, setDcaPs] = useState('1.5');
  const [dcaStepMult, setDcaStepMult] = useState('1.2');
  const [dcaMm, setDcaMm] = useState('1.5');
  const [dcaMaxSafety, setDcaMaxSafety] = useState('5');
  const [dcaTp, setDcaTp] = useState('1.0');
  const [dcaSl, setDcaSl] = useState('0');
  // recurring basket
  const [basket, setBasket] = useState([{ symbol: 'BTCUSDT', percent: '50' }, { symbol: 'ETHUSDT', percent: '50' }]);
  const [basketAmount, setBasketAmount] = useState('50');
  const [basketIntervalH, setBasketIntervalH] = useState('24');
  const [busy, setBusy] = useState(false);
  const toast = useToast();

  // Символы биржи для datalist — реальный список с API
  const [symbols, setSymbols] = useState([]);
  useEffect(() => {
    let alive = true;
    (async () => {
      try {
        const kind = type === 'recurring' ? 'spot' : 'futures';
        const r = await API().loadSymbols(exchange, kind);
        if (!alive) return;
        const list = (r?.symbols || r || [])
          .map(s => typeof s === 'string' ? s : (s.symbol || s.instId || ''))
          .map(s => s.replace(/[-_]/g, '').replace('SWAP', '').toUpperCase())
          .filter(s => /USDT$/.test(s));
        setSymbols([...new Set(list)].sort());
      } catch { if (alive) setSymbols([]); }
    })();
    return () => { alive = false; };
  }, [exchange, type]);

  const steps = ['Тип', 'Рынок', 'Параметры', 'Подтверждение'];
  const last = steps.length;
  const isSpotLike = type === 'recurring';

  const canNext = () => {
    if (step === 1) return !!type;
    if (step === 2) return isSpotLike || /^[A-Z0-9]{2,15}USDT$/.test((symbol || '').toUpperCase());
    return true;
  };

  const buildPayload = () => {
    const config = {};
    const investNum = parseFloat(type === 'recurring' ? basketAmount : invest) || 0;
    const levNum = isSpotLike ? 1 : (parseInt(lev) || 1);
    if (type === 'grid') {
      config.min_price = parseFloat(gridLow);
      config.max_price = parseFloat(gridHigh);
      config.grid_num = parseInt(gridNum);
      config.active_window = Math.max(1, parseInt(gridWindow) || 2);
      if (parseFloat(gridTp) > 0) config.tp_price = parseFloat(gridTp);
      if (parseFloat(gridSl) > 0) config.sl_price = parseFloat(gridSl);
      // продвинутые фичи
      if (gxTrailing) config.trailing = true;
      if (gxExpand) config.auto_expand = true;
      if (gxReinvest) config.reinvest = true;
      if (gxDynamic) config.dynamic_step = true;
      if (gxNotify) config.notify_cycles = true;
      if (parseFloat(gxMaxHours) > 0) config.max_runtime_hours = parseFloat(gxMaxHours);
      if (parseFloat(gxTpRoi) > 0) config.tp_roi_pct = parseFloat(gxTpRoi);
    }
    if (type === 'dca') {
      config.initial_margin = parseFloat(dcaIm);
      config.safety_margin = parseFloat(dcaSm);
      config.price_step_pct = parseFloat(dcaPs);
      config.price_step_multiplier = parseFloat(dcaStepMult) || 1.0;
      config.margin_multiplier = parseFloat(dcaMm) || 1.5;
      config.max_safety_orders = parseInt(dcaMaxSafety);
      config.tp_per_cycle_pct = parseFloat(dcaTp);
      config.sl_pct = parseFloat(dcaSl) || 0;
      config.direction = direction;
    }
    if (type === 'recurring') {
      config.basket = basket
        .filter(b => b.symbol && parseFloat(b.percent) > 0)
        .map(b => ({ symbol: b.symbol.toUpperCase(), percent: parseFloat(b.percent) }));
      config.amount_per_purchase = parseFloat(basketAmount);
      config.interval_hours = parseInt(basketIntervalH);
      config.market = 'spot';
    }
    return {
      exchange: exchange.toLowerCase(),
      symbol: type === 'recurring' ? 'BASKET' : (symbol || '').toUpperCase(),
      bot_type: type,
      direction: (type === 'dca' || type === 'recurring') ? (type === 'recurring' ? 'long' : direction) : direction,
      investment_usdt: investNum,
      leverage: levNum,
      config,
    };
  };

  const validate = () => {
    if (type === 'grid') {
      const lo = parseFloat(gridLow), hi = parseFloat(gridHigh), n = parseInt(gridNum);
      if (!(lo > 0 && hi > lo && n >= 2 && n <= 200)) { toast('Grid: проверь диапазон (low < high) и уровни 2–200', 'error'); return false; }
      const investNum = parseFloat(invest) || 0;
      const levNum = parseInt(lev) || 1;
      const perLevelNotional = investNum > 0 && n > 0 ? (investNum / n) * levNum : 0;
      if (perLevelNotional > 0 && perLevelNotional < 7.5) {
        toast(`Маловато: ${perLevelNotional.toFixed(2)} USDT нотионала/уровень (мин $7.5). Подними маржу/плечо или уменьши уровни`, 'error');
        return false;
      }
    }
    if (type === 'dca') {
      const ok = parseFloat(dcaIm) > 0 && parseFloat(dcaSm) > 0 && parseFloat(dcaPs) > 0 && parseFloat(dcaTp) > 0
        && parseInt(dcaMaxSafety) >= 1 && parseInt(dcaMaxSafety) <= 50;
      if (!ok) { toast('DCA: заполни маржу, шаг цены, TP и safety 1–50', 'error'); return false; }
    }
    if (type === 'recurring') {
      const totalPct = basket.reduce((s, b) => s + (parseFloat(b.percent) || 0), 0);
      if (!basket.length) { toast('Добавь хотя бы один токен в корзину', 'error'); return false; }
      if (Math.abs(totalPct - 100) > 0.01) { toast(`Сумма % должна быть 100 (сейчас ${totalPct.toFixed(1)})`, 'error'); return false; }
      if (!(parseFloat(basketAmount) > 0 && parseInt(basketIntervalH) >= 1)) { toast('Сумма > 0 и интервал ≥ 1ч', 'error'); return false; }
    }
    return true;
  };

  const submit = async () => {
    if (!validate()) return;
    setBusy(true);
    try {
      const payload = buildPayload();
      // Conflict-guard как на мобилке
      const check = await API().checkLocalBotConflict(payload);
      const conf = check && check.conflict;
      let ack = false;
      if (conf) {
        if (conf.severity === 'block') { toast(conf.message || 'Невозможно создать бот', 'error'); setBusy(false); return; }
        if (conf.severity === 'warn') {
          if (!window.confirm(conf.message + '\n\nВсё равно создать?')) { setBusy(false); return; }
          ack = true;
        }
        if (conf.severity === 'info') toast(conf.message, 'info');
      }
      await API().createLocalBot({ ...payload, acknowledge_conflict: ack });
      toast('Бот создан · нажми ▶ чтобы запустить', 'success');
      onCreated && onCreated();
      onClose();
    } catch (e) { toast('Не удалось создать: ' + (e.message || e), 'error'); }
    setBusy(false);
  };

  const next = () => step < last ? setStep(step + 1) : submit();

  return (
    <Modal title="Новый бот" sub={`Шаг ${step} из ${last} · ${steps[step - 1]}`} onClose={onClose} width={620}
      footer={<>
        {step > 1 && <button className="btn btn-ghost" disabled={busy} onClick={() => setStep(step - 1)}>Назад</button>}
        <button className="btn btn-primary" disabled={!canNext() || busy} onClick={next}>{busy ? 'Создаём…' : (step === last ? 'Создать бота' : 'Далее')}</button>
      </>}>
      <div style={{ display: 'flex', gap: 6, marginBottom: 22 }}>
        {steps.map((s, i) => <div key={i} style={{ flex: 1, height: 4, borderRadius: 99, background: i < step ? 'var(--accent)' : 'var(--bg-3)' }} />)}
      </div>

      {/* ШАГ 1 — тип */}
      {step === 1 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {BOT_TYPES.map(t => (
            <label key={t.id} className="card" style={{ padding: '14px 16px', display: 'flex', alignItems: 'center', gap: 14, cursor: 'pointer', borderColor: type === t.id ? 'var(--accent)' : 'var(--border)', background: type === t.id ? 'var(--accent-soft)' : 'var(--bg-2)' }} onClick={() => setType(t.id)}>
              <span style={{ color: type === t.id ? 'var(--accent)' : 'var(--text-muted)' }}><Icon name={t.icon} size={22} /></span>
              <div style={{ flex: 1 }}><div style={{ fontWeight: 600, fontSize: 14 }}>{t.label}</div><div className="muted" style={{ fontSize: 12 }}>{t.desc}</div></div>
              <div style={{ width: 16, height: 16, borderRadius: 99, border: '2px solid', borderColor: type === t.id ? 'var(--accent)' : 'var(--border-strong)', background: type === t.id ? 'var(--accent)' : 'transparent' }} />
            </label>
          ))}
          <div className="muted" style={{ fontSize: 11.5, marginTop: 4 }}>
            Торговля по сигналам из Telegram-каналов работает автоматически (Сигналы → Каналы) — отдельный бот для неё не нужен.
          </div>
        </div>
      )}

      {/* ШАГ 2 — рынок */}
      {step === 2 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
          <div>
            <div className="label-cap" style={{ marginBottom: 8 }}>Биржа</div>
            <div className="seg" style={{ width: '100%' }}>
              {['weex', 'okx', 'bybit', 'binance'].map(e => (
                <button key={e} style={{ flex: 1 }} className={exchange === e ? 'on' : ''} onClick={() => setExchange(e)}>{e.toUpperCase()}</button>
              ))}
            </div>
          </div>
          {!isSpotLike && (
            <div>
              <div className="label-cap" style={{ marginBottom: 8 }}>Символ</div>
              <SymbolPicker value={symbol} onChange={setSymbol} symbols={symbols} kind="fut" placeholder="напр. BTCUSDT" />
              <div className="muted" style={{ fontSize: 11, marginTop: 6 }}>
                {symbols.length ? `${symbols.length} инструментов с ${exchange.toUpperCase()}` : 'Загружаем список инструментов…'}
              </div>
            </div>
          )}
          {(type === 'grid' || type === 'dca') && (
            <div>
              <div className="label-cap" style={{ marginBottom: 8 }}>Направление</div>
              <div className="seg" style={{ width: '100%' }}>
                {[['long', 'LONG'], ['short', 'SHORT']].map(([k, l]) => (
                  <button key={k} style={{ flex: 1 }} className={direction === k ? 'on' : ''} onClick={() => setDirection(k)}>{l}</button>
                ))}
              </div>
            </div>
          )}
          {isSpotLike && (
            <div className="muted" style={{ fontSize: 12.5 }}>
              Корзина задаётся на следующем шаге — символ не нужен, бот будет покупать несколько монет сразу.
            </div>
          )}
        </div>
      )}

      {/* ШАГ 3 — параметры (ручной ввод, без ползунков) */}
      {step === 3 && type === 'grid' && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <div className="row" style={{ gap: 12 }}>
            <NumField style={{ flex: 1 }} label="Нижняя граница" value={gridLow} onChange={setGridLow} placeholder="4.80" />
            <NumField style={{ flex: 1 }} label="Верхняя граница" value={gridHigh} onChange={setGridHigh} placeholder="5.50" />
          </div>
          <div className="row" style={{ gap: 12 }}>
            <NumField style={{ flex: 1 }} label="Кол-во уровней" value={gridNum} onChange={setGridNum} placeholder="20" hint="2–200" />
            <NumField style={{ flex: 1 }} label="Инвестиция (маржа)" value={invest} onChange={setInvest} suffix="USDT" />
            <NumField style={{ flex: 1 }} label="Плечо" value={lev} onChange={setLev} placeholder="10" hint="1–50" />
          </div>
          <div className="row" style={{ gap: 12 }}>
            <NumField style={{ flex: 1 }} label="Активных ордеров (с каждой стороны)" value={gridWindow} onChange={setGridWindow} hint="2 = на бирже 2 buy + 2 sell, остальное в софте" />
            <NumField style={{ flex: 1 }} label="TP цена (опц.)" value={gridTp} onChange={setGridTp} placeholder="—" hint="Закрыть всё при цене" />
            <NumField style={{ flex: 1 }} label="SL цена (опц.)" value={gridSl} onChange={setGridSl} placeholder="—" hint="Стоп при цене" />
          </div>
          {(() => {
            const n = parseInt(gridNum) || 0, inv = parseFloat(invest) || 0, lv = parseInt(lev) || 1;
            const w = Math.max(1, parseInt(gridWindow) || 2);
            const perLvl = n > 0 ? (inv / n) * lv : 0;
            // Начальная позиция теперь ≈ w уровней (а не вся верхняя половина)
            const startNotional = perLvl * w;
            return perLvl > 0 && (
              <div className="card" style={{ padding: '10px 14px', background: perLvl < 7.5 ? 'var(--red-soft)' : 'var(--bg-2)', border: perLvl < 7.5 ? '1px solid var(--red)' : '1px solid var(--border)', fontSize: 12, lineHeight: 1.6 }}>
                Нотионал на уровень: <b className="mono">{perLvl.toFixed(2)} USDT</b>{perLvl < 7.5 ? ' (мин $7.5!)' : ''}<br />
                Старт. позиция ≈ <b className="mono">{startNotional.toFixed(0)} USDT</b> (только окно) · остальное докупается по мере падения → ликвидация далеко<br />
                На бирже: <b className="mono">{w * 2}</b> ордеров из {n} (остальные в софте)
              </div>
            );
          })()}
          {/* Продвинутые фичи */}
          <div className="divider" />
          <div className="label-cap">Продвинутое (опционально)</div>
          {[
            [gxTrailing, setGxTrailing, 'Trailing-сетка', 'Сетка следует за ценой в тренде — не застревает на границе'],
            [gxExpand, setGxExpand, 'Авто-расширение диапазона', 'Если цена пробила границу — расширяет сетку вместо простоя'],
            [gxReinvest, setGxReinvest, 'Реинвест профита', 'Заработок с витков добавляется в позицию (сложный процент)'],
            [gxDynamic, setGxDynamic, 'Динамический шаг', 'Гуще сетка у текущей цены — больше витков в зоне волатильности'],
            [gxNotify, setGxNotify, 'Пуш на каждый виток', 'Уведомление в браузер/бота при каждом отработанном витке сетки'],
          ].map(([val, set, title, desc]) => (
            <div key={title} className="row" style={{ gap: 12, padding: '8px 0', borderBottom: '1px solid var(--border)' }}>
              <div style={{ flex: 1 }}>
                <div style={{ fontWeight: 600, fontSize: 13 }}>{title}</div>
                <div className="muted" style={{ fontSize: 11.5, marginTop: 1 }}>{desc}</div>
              </div>
              <div className={cx('tgl', val && 'on')} onClick={() => set(v => !v)} />
            </div>
          ))}
          <div className="row" style={{ gap: 12 }}>
            <NumField style={{ flex: 1 }} label="Стоп по времени (часов)" value={gxMaxHours} onChange={setGxMaxHours} placeholder="0 = выкл" hint="Закрыть бота через N часов" />
            <NumField style={{ flex: 1 }} label="Стоп по профиту (% ROI)" value={gxTpRoi} onChange={setGxTpRoi} placeholder="0 = выкл" hint="Закрыть при +X% на инвест" />
          </div>
        </div>
      )}
      {step === 3 && type === 'dca' && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <div className="row" style={{ gap: 12 }}>
            <NumField style={{ flex: 1 }} label="Начальная маржа" value={dcaIm} onChange={setDcaIm} suffix="USDT" />
            <NumField style={{ flex: 1 }} label="Маржа safety-ордера" value={dcaSm} onChange={setDcaSm} suffix="USDT" />
            <NumField style={{ flex: 1 }} label="Плечо" value={lev} onChange={setLev} placeholder="10" />
          </div>
          <div className="row" style={{ gap: 12 }}>
            <NumField style={{ flex: 1 }} label="Шаг цены %" value={dcaPs} onChange={setDcaPs} hint="Расстояние до 1-го safety" />
            <NumField style={{ flex: 1 }} label="Мульт. шага" value={dcaStepMult} onChange={setDcaStepMult} hint="×1.2 = шаг растёт" />
            <NumField style={{ flex: 1 }} label="Мульт. маржи" value={dcaMm} onChange={setDcaMm} hint="×1.5 мартингейл" />
          </div>
          <div className="row" style={{ gap: 12 }}>
            <NumField style={{ flex: 1 }} label="Макс. safety" value={dcaMaxSafety} onChange={setDcaMaxSafety} hint="1–50" />
            <NumField style={{ flex: 1 }} label="TP за цикл %" value={dcaTp} onChange={setDcaTp} hint="Профит цикла" />
            <NumField style={{ flex: 1 }} label="Глобальный SL % (опц.)" value={dcaSl} onChange={setDcaSl} placeholder="0" hint="0 = выкл" />
          </div>
        </div>
      )}
      {step === 3 && type === 'recurring' && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <div className="label-cap">Корзина (сумма % = 100)</div>
          {basket.map((b, i) => (
            <div key={i} className="row" style={{ gap: 10, alignItems: 'flex-start' }}>
              <SymbolPicker style={{ flex: 2 }} value={b.symbol} kind="spot" placeholder="BTCUSDT"
                symbols={symbols} onChange={(v) => setBasket(basket.map((x, j) => j === i ? { ...x, symbol: v } : x))} />
              <div style={{ position: 'relative', flex: 1 }}>
                <input className="inp mono" type="text" inputMode="decimal" value={b.percent}
                  onChange={e => setBasket(basket.map((x, j) => j === i ? { ...x, percent: e.target.value } : x))} placeholder="50" />
                <span className="muted mono" style={{ position: 'absolute', right: 12, top: 10, fontSize: 12 }}>%</span>
              </div>
              <button className="btn btn-ghost btn-icon btn-sm" onClick={() => setBasket(basket.filter((_, j) => j !== i))}><Icon name="trash" size={14} /></button>
            </div>
          ))}
          <button className="btn btn-ghost btn-sm" style={{ alignSelf: 'flex-start' }} onClick={() => setBasket([...basket, { symbol: '', percent: '' }])}><Icon name="plus" size={14} />Добавить токен</button>
          <div className="row" style={{ gap: 12 }}>
            <NumField style={{ flex: 1 }} label="Сумма закупки" value={basketAmount} onChange={setBasketAmount} suffix="USDT" hint="За одну покупку" />
            <NumField style={{ flex: 1 }} label="Интервал (часы)" value={basketIntervalH} onChange={setBasketIntervalH} hint="24 = раз в день" />
          </div>
          {(() => {
            const total = basket.reduce((s, b) => s + (parseFloat(b.percent) || 0), 0);
            return (
              <div className="card" style={{ padding: '10px 14px', background: Math.abs(total - 100) > 0.01 ? 'var(--amber-soft)' : 'var(--green-soft)', border: `1px solid ${Math.abs(total - 100) > 0.01 ? 'var(--amber)' : 'var(--green)'}`, fontSize: 12 }}>
                Сумма долей: <b className="mono">{total.toFixed(1)}%</b>{Math.abs(total - 100) > 0.01 ? ' — должно быть 100%' : ' ✓'}
              </div>
            );
          })()}
        </div>
      )}

      {/* ШАГ 4 — подтверждение */}
      {step === 4 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {[
            ['Тип', BOT_TYPES.find(t => t.id === type)?.label],
            ['Биржа', exchange.toUpperCase()],
            !isSpotLike && ['Символ', (symbol || '').toUpperCase()],
            (type === 'grid' || type === 'dca') && ['Направление', direction.toUpperCase()],
            type === 'grid' && ['Диапазон', `${gridLow} – ${gridHigh}`],
            type === 'grid' && ['Уровней', gridNum],
            type === 'dca' && ['Маржа (нач/safety)', `${dcaIm} / ${dcaSm} USDT`],
            type === 'dca' && ['Safety макс.', dcaMaxSafety],
            type === 'recurring' && ['Корзина', basket.map(b => `${b.symbol} ${b.percent}%`).join(' · ')],
            type === 'recurring' && ['Закупка', `${basketAmount} USDT каждые ${basketIntervalH}ч`],
            ['Инвестиция', `${type === 'recurring' ? basketAmount : invest} USDT`],
            !isSpotLike && ['Плечо', '×' + lev],
          ].filter(Boolean).map(([l, v]) => (
            <div key={l} className="row between" style={{ padding: '10px 0', borderBottom: '1px solid var(--border)' }}>
              <span className="muted" style={{ fontSize: 13 }}>{l}</span><span className="mono" style={{ fontWeight: 600, textAlign: 'right', maxWidth: '60%' }}>{v}</span>
            </div>
          ))}
          <div className="card" style={{ padding: '12px 14px', background: 'var(--accent-soft)', border: '1px solid var(--accent-ring)', marginTop: 6, fontSize: 12.5 }}>
            Бот создастся в статусе «остановлен» — запусти его кнопкой ▶ на карточке.
          </div>
        </div>
      )}
    </Modal>
  );
}

/* ---- Детали локального бота: live-данные с /api/me/local-bots/{id} ---- */
function LocalBotDetail({ b, onClose, onChanged }) {
  const [tab, setTab] = useState('overview');
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [busy, setBusy] = useState(false);
  const toast = useToast();
  const reload = async () => {
    try { const r = await API().loadLocalBotDetails(b.id); setData(r); } catch {}
    setLoading(false);
  };
  useEffect(() => { reload(); const t = setInterval(reload, 15000); return () => clearInterval(t); }, [b.id]);

  const raw = data || {};
  const bot = raw.bot || raw;
  const st = raw.state || bot.state_json || bot.runtime || {};
  const cfg = bot.config || raw.config || (() => { try { return JSON.parse(bot.config_json || '{}'); } catch { return {}; } })();
  const fills = raw.fills || bot.fills || [];
  const markPx = parseFloat(raw.mark_px || raw.markPx || st.mark_px || 0);
  const avgEntry = parseFloat(st.avg_entry || 0);
  const totalQty = parseFloat(st.total_qty || 0);
  const totalMargin = parseFloat(st.total_margin || bot.investment_usdt || b.invest || 0);
  const lev = parseInt(bot.leverage || b.lev || 1);
  const dir = String(bot.direction || 'long').toLowerCase();
  const sign = dir === 'long' ? 1 : -1;
  const floatPnl = (markPx > 0 && avgEntry > 0) ? (markPx - avgEntry) * totalQty * sign : 0;
  const realizedPnl = parseFloat(bot.total_pnl || 0);
  const totalPnl = realizedPnl + floatPnl;
  const pnlPct = totalMargin > 0 ? totalPnl / totalMargin * 100 : 0;
  const liqPx = (avgEntry > 0 && lev > 1)
    ? (dir === 'long' ? avgEntry * (1 - (1 / lev - 0.005)) : avgEntry * (1 + (1 / lev - 0.005)))
    : 0;
  const isDca = (bot.bot_type || b.type) === 'dca';
  const isGrid = (bot.bot_type || b.type) === 'grid';
  const safetyDone = fills.filter(f => f.type === 'safety').length;
  const maxSafety = parseInt(cfg.max_safety_orders || 0);
  // Grid-специфика как у OKX
  const cyclesDone = parseInt(st.cycles_done || 0);           // отработанных витков сетки (≈ arbitrageNum)
  const gridProfit = realizedPnl;                              // realized grid-профит копится в total_pnl
  const notional = (avgEntry > 0 && totalQty > 0) ? totalQty * avgEntry : 0;
  // время работы
  const startTs = bot.started_at || bot.created_at;
  const refTs = (b.status === 'running') ? Date.now() : (bot.stopped_at || Date.now());
  const uptimeMs = startTs ? Math.max(0, refTs - startTs) : 0;
  const uptimeStr = uptimeMs > 0
    ? (uptimeMs >= 86400000 ? Math.floor(uptimeMs / 86400000) + 'д ' + Math.floor((uptimeMs % 86400000) / 3600000) + 'ч'
       : uptimeMs >= 3600000 ? Math.floor(uptimeMs / 3600000) + 'ч ' + Math.floor((uptimeMs % 3600000) / 60000) + 'м'
       : Math.floor(uptimeMs / 60000) + 'м')
    : '—';
  const gridLow = parseFloat(cfg.min_price || 0);
  const gridHigh = parseFloat(cfg.max_price || 0);
  const gridNum = parseInt(cfg.grid_num || 0);

  const act = async (fn, okMsg) => {
    setBusy(true);
    try { await fn(); toast(okMsg, 'success'); await reload(); onChanged && onChanged(); }
    catch (e) { toast('Ошибка: ' + (e.message || e), 'error'); }
    setBusy(false);
  };

  return (
    <Modal title={`${bot.symbol || b.symbol}`} sub={`${(bot.bot_type || b.type || '').toUpperCase()} · ${(bot.exchange || b.exchange || '').toUpperCase()} · ${dir} ${lev > 1 ? '· ×' + lev : ''}`} onClose={onClose} width={680}
      footer={<>
        <button className="btn btn-ghost" onClick={onClose}>Закрыть</button>
        {isDca && b.status === 'running' && (
          <button className="btn btn-ghost" disabled={busy} onClick={() => act(() => API().dcaManualSafety(b.id), 'Safety-ордер размещён')}>
            <Icon name="shield" size={14} />Manual Safety
          </button>
        )}
        <button className="btn btn-ghost" style={{ color: 'var(--red)' }} disabled={busy}
          onClick={() => { if (confirm('Удалить бота? Позиции НЕ закрываются автоматически.')) act(() => API().deleteLocalBot(b.id), 'Бот удалён').then(onClose); }}>
          <Icon name="trash" size={14} />Удалить
        </button>
        <button className={cx('btn', b.status === 'running' ? 'btn-danger' : 'btn-primary')} disabled={busy}
          onClick={() => act(() => b.status === 'running' ? API().stopLocalBot(b.id) : API().startLocalBot(b.id), b.status === 'running' ? 'Бот остановлен' : 'Бот запущен')}>
          {b.status === 'running' ? 'Остановить' : 'Запустить'}
        </button>
      </>}>
      {/* Big PnL header */}
      <div className="card" style={{ padding: '14px 18px', background: 'var(--bg-2)', marginBottom: 16 }}>
        <div className="row between">
          <span className="muted" style={{ fontSize: 12 }}>PnL (реализ. + плавающий)</span>
          <span className={cx('mono', pn(totalPnl))} style={{ fontSize: 24, fontWeight: 700 }}>
            {fmt.sign(totalPnl)}{fmt.usdt(totalPnl)} USDT
            <span style={{ fontSize: 13, marginLeft: 8, opacity: .7 }}>({fmt.pct(pnlPct)})</span>
          </span>
        </div>
      </div>

      <div className="tabs" style={{ marginBottom: 16 }}>
        {[['overview', 'Обзор'], ['params', 'Параметры'], ['fills', `Ордера (${fills.length})`]].map(([k, l]) => (
          <button key={k} className={tab === k ? 'on' : ''} onClick={() => setTab(k)}>{l}</button>
        ))}
      </div>

      {loading && <div className="skel" style={{ height: 200, borderRadius: 12 }} />}

      {!loading && tab === 'overview' && (
        <>
          {/* KPI-плитки как у OKX */}
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 10, marginBottom: 14 }}>
            {[
              ['Grid-профит', <span className={pn(gridProfit)}>{fmt.sign(gridProfit)}{fmt.usdt(gridProfit)}</span>],
              ['Float PnL', <span className={pn(floatPnl)}>{fmt.sign(floatPnl)}{fmt.usdt(floatPnl)}</span>],
              [isGrid ? 'Витков сетки' : 'Сделок', isGrid ? cyclesDone : (b.tradesCount || cyclesDone)],
              ['Работает', uptimeStr],
            ].map(([l, v], i) => (
              <div key={i} className="card" style={{ padding: '10px 12px', background: 'var(--bg-2)' }}>
                <div className="label-cap" style={{ fontSize: 9.5 }}>{l}</div>
                <div className="mono" style={{ fontSize: 15, fontWeight: 700, marginTop: 3 }}>{v}</div>
              </div>
            ))}
          </div>
          {/* Текущая позиция */}
          <div className="card card-pad" style={{ background: 'var(--bg-2)', marginBottom: 14 }}>
            <div className="label-cap" style={{ marginBottom: 8 }}>Текущая позиция</div>
            {[
              ['Текущая цена', markPx > 0 ? markPx : '—'],
              ['Средняя цена входа', avgEntry > 0 ? avgEntry.toFixed(6) : '—'],
              ['Размер позиции', totalQty > 0 ? `${totalQty.toFixed(4)} ${(bot.symbol || '').replace(/USDT$/, '')}` : '—'],
              ['Нотионал', notional > 0 ? fmt.usdt(notional) + ' USDT' : '—'],
              ['Инвестиция / маржа', fmt.usdt(totalMargin) + ' USDT'],
              ['Плечо', '×' + lev],
              liqPx > 0 && ['Цена ликвидации (~)', <span style={{ color: 'var(--amber)' }}>{liqPx.toPrecision(6)}</span>],
              isDca && maxSafety > 0 && ['Safety-ордеров', `${safetyDone} / ${maxSafety}`],
            ].filter(Boolean).map(([l, v], i) => (
              <div key={i} className="row between" style={{ padding: '7px 0', borderBottom: '1px solid var(--border)' }}>
                <span className="muted" style={{ fontSize: 12.5 }}>{l}</span>
                <span className="mono" style={{ fontSize: 13.5, fontWeight: 600 }}>{v}</span>
              </div>
            ))}
          </div>
          {/* Параметры сетки (только grid) */}
          {isGrid && gridLow > 0 && (
            <div className="card card-pad" style={{ background: 'var(--bg-2)' }}>
              <div className="label-cap" style={{ marginBottom: 8 }}>Сетка</div>
              {[
                ['Диапазон', `${gridLow} – ${gridHigh}`],
                ['Уровней', gridNum],
                ['Шаг сетки', gridNum > 0 ? ((gridHigh - gridLow) / gridNum).toFixed(6) : '—'],
                cfg.active_window > 0 && ['Активных ордеров на бирже', `${cfg.active_window * 2} (${cfg.active_window}+${cfg.active_window}) из ${gridNum}`],
                ['Цена в диапазоне', markPx > 0 ? (markPx >= gridLow && markPx <= gridHigh ? '✓ внутри' : '⚠ вне диапазона') : '—'],
              ].filter(Boolean).map(([l, v], i) => (
                <div key={i} className="row between" style={{ padding: '7px 0', borderBottom: '1px solid var(--border)' }}>
                  <span className="muted" style={{ fontSize: 12.5 }}>{l}</span>
                  <span className="mono" style={{ fontSize: 13.5, fontWeight: 600 }}>{v}</span>
                </div>
              ))}
            </div>
          )}
        </>
      )}

      {!loading && tab === 'params' && (
        <div className="card card-pad" style={{ background: 'var(--bg-2)' }}>
          {Object.entries(cfg).map(([k, v]) => (
            <div key={k} className="row between" style={{ padding: '8px 0', borderBottom: '1px solid var(--border)' }}>
              <span className="muted mono" style={{ fontSize: 12 }}>{k}</span>
              <span className="mono" style={{ fontSize: 13 }}>{typeof v === 'object' ? JSON.stringify(v) : String(v)}</span>
            </div>
          ))}
          {!Object.keys(cfg).length && <Empty icon="info" title="Конфиг пуст" />}
        </div>
      )}

      {!loading && tab === 'fills' && (
        fills.length ? (
          <table className="tbl">
            <thead><tr><th>Тип</th><th className="num">Цена</th><th className="num">Кол-во</th><th className="num">Время</th></tr></thead>
            <tbody>{fills.map((f, i) => (
              <tr key={i}>
                <td><span className="pill pill-soft">{f.type || 'fill'}</span></td>
                <td className="num mono">{f.price || f.px || '—'}</td>
                <td className="num mono">{f.qty || f.size || '—'}</td>
                <td className="num muted mono" style={{ fontSize: 11.5 }}>{f.time || f.ts ? new Date(f.time || f.ts).toLocaleString('ru-RU', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) : '—'}</td>
              </tr>
            ))}</tbody>
          </table>
        ) : <Empty icon="layers" title="Ордеров пока нет" text="Появятся после запуска бота." />
      )}
    </Modal>
  );
}

/* ---- Детали биржевого бота (OKX algo): live с /api/ex/{ex}/bots/details ---- */
function ExchangeBotDetail({ b, onClose }) {
  const [data, setData] = useState(null);
  const [orders, setOrders] = useState([]);
  const [loading, setLoading] = useState(true);
  const toast = useToast();
  useEffect(() => {
    let stop = false;
    (async () => {
      try {
        const r = await API().loadBotDetails((b.exchange || 'okx').toLowerCase(), b.algoId, b.algoOrdType || 'contract_grid');
        if (stop) return;
        const d = r?.details || r?.detail || r || {};
        setData({ ...d, markPx: r?.markPx || d.markPx });
        setOrders(r?.subOrders || r?.sub_orders || []);
      } catch (e) { if (!stop) toast('Не удалось загрузить детали: ' + (e.message || e), 'error'); }
      if (!stop) setLoading(false);
    })();
    return () => { stop = true; };
  }, [b.algoId]);

  const _n = (v) => { const n = parseFloat(v); return Number.isFinite(n) ? n : 0; };
  const d = data || {};
  const totalPnl = _n(d.totalPnl) || b.pnl || 0;
  const pnlRatio = _n(d.pnlRatio) * 100;
  const gridProfit = _n(d.gridProfit);
  const floatProfit = _n(d.floatProfit);
  const fundingFee = _n(d.fundingFee);
  const fee = _n(d.fee);
  const investment = _n(d.investment || d.sz) || b.invest;
  const runtimeDays = (_n(d.uTime) && _n(d.cTime)) ? ((_n(d.uTime) - _n(d.cTime)) / 86400000) : 0;
  const annualRate = _n(d.totalAnnualizedRate || d.annualizedRate) * 100;

  return (
    <Modal title={`${b.symbol} · OKX Grid`} sub={`algoId ${b.algoId || '—'} · управляется биржей`} onClose={onClose} width={680}
      footer={<button className="btn btn-ghost" onClick={onClose}>Закрыть</button>}>
      <div className="card" style={{ padding: '14px 18px', background: 'var(--bg-2)', marginBottom: 16 }}>
        <div className="row between">
          <span className="muted" style={{ fontSize: 12 }}>Total PnL</span>
          <span className={cx('mono', pn(totalPnl))} style={{ fontSize: 24, fontWeight: 700 }}>
            {fmt.sign(totalPnl)}{fmt.usdt(totalPnl)} USDT
            {pnlRatio !== 0 && <span style={{ fontSize: 13, marginLeft: 8, opacity: .7 }}>({fmt.pct(pnlRatio)})</span>}
          </span>
        </div>
      </div>
      {loading ? <div className="skel" style={{ height: 280, borderRadius: 12 }} /> : <>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 10, marginBottom: 16 }}>
          {[
            ['Grid-профит', <span className="pos">+{fmt.usdt(gridProfit)}</span>],
            ['Float PnL', <span className={pn(floatProfit)}>{fmt.sign(floatProfit)}{fmt.usdt(floatProfit)}</span>],
            ['Funding', <span className={pn(fundingFee)}>{fmt.sign(fundingFee)}{fmt.usdt(fundingFee)}</span>],
            ['Комиссии', fmt.usdt(Math.abs(fee))],
          ].map(([l, v], i) => (
            <div key={i} className="card" style={{ padding: '10px 12px', background: 'var(--bg-2)' }}>
              <div className="label-cap" style={{ fontSize: 9.5 }}>{l}</div>
              <div className="mono" style={{ fontSize: 15, fontWeight: 700, marginTop: 3 }}>{v}</div>
            </div>
          ))}
        </div>
        <div className="card card-pad" style={{ background: 'var(--bg-2)', marginBottom: 16 }}>
          {[
            ['Инвестиция', fmt.usdt(investment) + ' USDT'],
            ['Эквити бота', _n(d.eq) > 0 ? fmt.usdt(_n(d.eq)) + ' USDT' : '—'],
            ['Плечо (выставл./факт.)', `×${_n(d.lever) || b.lev}${_n(d.actualLever) ? ' / ×' + _n(d.actualLever).toFixed(2) : ''}`],
            ['Диапазон сетки', _n(d.minPx) > 0 ? `${_n(d.minPx)} – ${_n(d.maxPx)}` : '—'],
            ['Уровней сетки', _n(d.gridNum) || '—'],
            ['Активных ордеров', _n(d.activeOrdNum) || 0],
            ['Цена входа (start)', _n(d.runPx) || '—'],
            ['Текущая цена', _n(d.markPx) || '—'],
            ['Цена ликвидации', _n(d.liqPx) > 0 ? <span style={{ color: 'var(--amber)' }}>{_n(d.liqPx)}</span> : '—'],
            ['Арбитражей сетки', _n(d.arbitrageNum) || 0],
            ['Всего сделок', _n(d.tradeNum) || 0],
            ['Годовая доходность', annualRate !== 0 ? <span className={pn(annualRate)}>{fmt.pct(annualRate)}</span> : '—'],
            ['Работает', runtimeDays > 0 ? runtimeDays.toFixed(1) + ' дн' : '—'],
            _n(d.tpTriggerPx) > 0 && ['TP триггер', _n(d.tpTriggerPx)],
            _n(d.slTriggerPx) > 0 && ['SL триггер', _n(d.slTriggerPx)],
          ].filter(Boolean).map(([l, v], i) => (
            <div key={i} className="row between" style={{ padding: '7px 0', borderBottom: '1px solid var(--border)' }}>
              <span className="muted" style={{ fontSize: 12.5 }}>{l}</span>
              <span className="mono" style={{ fontSize: 13, fontWeight: 600 }}>{v}</span>
            </div>
          ))}
        </div>
        <div className="label-cap" style={{ marginBottom: 8 }}>Sub-ордера ({orders.length})</div>
        {orders.length ? (
          <div style={{ maxHeight: 220, overflowY: 'auto' }} className="card">
            <table className="tbl">
              <thead><tr><th>Сторона</th><th className="num">Цена</th><th className="num">Размер</th><th>Статус</th></tr></thead>
              <tbody>{orders.slice(0, 50).map((o, i) => (
                <tr key={i}>
                  <td><span className={cx('badge', /buy/i.test(o.side || '') ? 'badge-long' : 'badge-short')}>{(o.side || '').toUpperCase()}</span></td>
                  <td className="num mono">{o.px || o.price || '—'}</td>
                  <td className="num mono">{o.sz || o.size || '—'}</td>
                  <td><span className="pill pill-soft" style={{ fontSize: 10 }}>{o.state || o.status || '—'}</span></td>
                </tr>
              ))}</tbody>
            </table>
          </div>
        ) : <Empty icon="layers" title="Sub-ордеров нет" text="OKX вернул пустой список." />}
      </>}
    </Modal>
  );
}

function BotDetail({ b, onClose, onChanged }) {
  return b.kind === 'exchange'
    ? <ExchangeBotDetail b={b} onClose={onClose} />
    : <LocalBotDetail b={b} onClose={onClose} onChanged={onChanged} />;
}

function Bots() {
  const [segment, setSegment] = useState('local');
  const [filter, setFilter] = useState('all');
  const [wizard, setWizard] = useState(false);
  const [detail, setDetail] = useState(null);
  const [tick, setTick] = useState(0);
  const reloadBots = () => setTick(t => t + 1);
  const [localRaw] = useLiveData(() => API().listLocalBots(), null, [tick], 30000);
  const [exchangeRaw] = useLiveData(() => API().loadBotsActive(), null, [tick], 60000);
  const toast = useToast();
  const bots = {
    local: (localRaw || []).map(mapLocalBot),
    exchange: (exchangeRaw || []).map(mapExchangeBot),
  };
  const list = bots[segment];
  const shown = list.filter(b => filter === 'all' || b.status === filter);
  const toggle = async (b) => {
    if (segment !== 'local') { toast('Биржевыми ботами управляйте на бирже', 'info'); return; }
    try {
      if (b.status === 'running') { await API().stopLocalBot(b.id); toast('Бот остановлен', 'info'); }
      else { await API().startLocalBot(b.id); toast('Бот запущен', 'success'); }
      reloadBots();
    } catch { toast('Не удалось переключить бота', 'error'); }
  };
  const counts = { all: list.length, running: list.filter(b => b.status === 'running').length, paused: list.filter(b => b.status === 'paused').length, stopped: list.filter(b => b.status === 'stopped').length };
  const sumInvest = list.reduce((s, b) => s + b.invest, 0);
  const sumPnl = list.reduce((s, b) => s + b.pnl, 0);

  return (
    <div className="fade-in" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 18 }}>
      {/* Top segment switcher: Наши / С биржи */}
      <div className="row between">
        <div className="seg emerald" style={{ background: 'var(--bg-1)', padding: 4 }}>
          <button className={segment === 'local' ? 'on' : ''} onClick={() => setSegment('local')} style={{ padding: '0 18px', height: 36 }}>
            <Icon name="bot" size={14} style={{ marginRight: 6 }} />Наши боты <span className="mono" style={{ opacity: .6, marginLeft: 6 }}>{bots.local.length}</span>
          </button>
          <button className={segment === 'exchange' ? 'on' : ''} onClick={() => setSegment('exchange')} style={{ padding: '0 18px', height: 36 }}>
            <Icon name="exchange" size={14} style={{ marginRight: 6 }} />С биржи <span className="mono" style={{ opacity: .6, marginLeft: 6 }}>{bots.exchange.length}</span>
          </button>
        </div>
        {segment === 'local' && (
          <button className="btn btn-primary" onClick={() => setWizard(true)}><Icon name="plus" size={16} />Новый бот</button>
        )}
      </div>

      {/* KPI strip */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16 }}>
        <Kpi label="Запущено" value={counts.running} sub={`из ${counts.all}`} icon="bot" accent="var(--emerald)" />
        <Kpi label="Суммарный PnL" value={<span className={pn(sumPnl)}>{fmt.sign(sumPnl)}{fmt.usdt(sumPnl)}</span>} sub="USDT" icon="trendup" accent={sumPnl > 0 ? 'var(--green)' : 'var(--red)'} />
        <Kpi label="Инвестировано" value={fmt.usdt(sumInvest, 0)} sub="USDT" icon="wallet" accent="var(--accent)" />
        <Kpi label="Win Rate · 7D" value="64%" sub="14W / 8L" icon="target" accent="var(--green)" />
      </div>

      {/* Filter chips */}
      <div className="row between">
        <div className="seg">
          {[['all', 'Все'], ['running', 'Работают'], ['paused', 'Пауза'], ['stopped', 'Остановлены']].map(([k, l]) => (
            <button key={k} className={filter === k ? 'on' : ''} onClick={() => setFilter(k)}>{l} <span className="mono" style={{ opacity: .6 }}>{counts[k]}</span></button>
          ))}
        </div>
        {segment === 'exchange' && <span className="pill pill-soft"><Icon name="info" size={12} />Биржевые боты управляются через API биржи</span>}
      </div>

      {shown.length ? (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: 16 }}>
          {shown.map(b => <BotCard key={b.id} b={b} onOpen={setDetail} onToggle={toggle} />)}
        </div>
      ) : (
        <div className="card">
          <Empty
            icon={segment === 'local' ? 'bot' : 'exchange'}
            title={segment === 'local' ? 'Нет ботов API4ATKA' : 'Нет ботов на бирже'}
            text={segment === 'local' ? 'Создай Grid/DCA/Сигнальный бота на нашей инфраструктуре.' : 'Биржевые алго-боты (OKX и др.) появятся здесь.'}
            action={segment === 'local' && <button className="btn btn-primary" onClick={() => setWizard(true)}><Icon name="plus" size={16} />Новый бот</button>}
          />
        </div>
      )}

      {wizard && <CreateBotWizard onClose={() => setWizard(false)} onCreated={reloadBots} />}
      {detail && <BotDetail b={detail} onClose={() => setDetail(null)} onChanged={reloadBots} />}
    </div>
  );
}

/* ===================== COPY TRADING ===================================== */
function CopySubscribeModal({ m, accounts, onClose, onDone }) {
  const [step, setStep] = useState(1);
  const [exchange, setExchange] = useState((accounts[0] || {}).id || 'weex');
  const [mode, setMode] = useState('auto');
  const [sizeMode, setSizeMode] = useState('pct_alloc');
  const [sizeValue, setSizeValue] = useState('2');
  const [allocation, setAllocation] = useState('500');
  const [copyExisting, setCopyExisting] = useState('new_only');
  const [maxLev, setMaxLev] = useState('0');
  const [stopLoss, setStopLoss] = useState('0');
  const [busy, setBusy] = useState(false);
  const toast = useToast();
  const steps = ['Аккаунт', 'Режим', 'Размер', 'Существующие', 'Подтверждение'];
  const last = steps.length;

  const submit = async () => {
    setBusy(true);
    try {
      const r = await fetch('/api/me/copytrade/subscribe', {
        method: 'POST', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          master_id: m.master_id,
          follower_exchange: exchange,
          mode,
          size_mode: sizeMode,
          size_value: parseFloat(sizeValue) || 1,
          allocation_usdt: parseFloat(allocation) || 0,
          copy_existing: copyExisting,
          max_leverage: parseInt(maxLev) || 0,
          stop_loss_pct: parseFloat(stopLoss) || 0,
        }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.detail || 'Ошибка ' + r.status);
      toast(`Копирование ${m.nickname} запущено`, 'success');
      onDone();
    } catch (e) { toast('Не удалось: ' + (e.message || e), 'error'); }
    setBusy(false);
  };

  const next = () => step < last ? setStep(step + 1) : submit();
  const exampleSize = sizeMode === 'fixed' ? `${sizeValue} USDT нотионала на сделку`
    : sizeMode === 'proportional' ? `×${sizeValue} от размера мастера`
    : `${(parseFloat(allocation) * parseFloat(sizeValue) / 100 || 0).toFixed(2)} USDT маржи (${sizeValue}% от ${allocation}) × плечо`;

  return (
    <Modal title={`Копировать · ${m.nickname}`} sub={`Шаг ${step} из ${last} · ${steps[step - 1]}`} onClose={onClose} width={580}
      footer={<>
        {step > 1 && <button className="btn btn-ghost" disabled={busy} onClick={() => setStep(step - 1)}>Назад</button>}
        <button className="btn btn-primary" disabled={busy} onClick={next}>{busy ? 'Запускаем…' : (step === last ? 'Начать копирование' : 'Далее')}</button>
      </>}>
      <div style={{ display: 'flex', gap: 6, marginBottom: 22 }}>
        {steps.map((s, i) => <div key={i} style={{ flex: 1, height: 4, borderRadius: 99, background: i < step ? 'var(--accent)' : 'var(--bg-3)' }} />)}
      </div>

      {step === 1 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          <div className="label-cap">На какой биржевой аккаунт зеркалить сделки</div>
          {accounts.length ? accounts.map(c => (
            <label key={c.id} className="card" style={{ padding: '14px 16px', display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer', borderColor: exchange === c.id ? 'var(--accent)' : 'var(--border)', background: exchange === c.id ? 'var(--accent-soft)' : 'var(--bg-2)' }} onClick={() => setExchange(c.id)}>
              <span style={{ color: 'var(--accent)' }}><Icon name="exchange" size={20} /></span>
              <div style={{ flex: 1 }}><div style={{ fontWeight: 600, fontSize: 14 }}>{c.name}</div><div className="muted mono" style={{ fontSize: 11.5 }}>{c.key}</div></div>
            </label>
          )) : <div className="card" style={{ padding: '12px 14px', background: 'var(--amber-soft)', border: '1px solid var(--amber)', fontSize: 12.5 }}>Нет подключённых бирж. Сначала добавь API-ключ в Настройки → API ключи.</div>}
          <div className="muted" style={{ fontSize: 11.5 }}>Мастер торгует на {m.exchange?.toUpperCase()}. Зеркалить можно на любую твою биржу.</div>
        </div>
      )}

      {step === 2 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          <div className="label-cap">Режим копирования</div>
          <label className="card" style={{ padding: '14px 16px', display: 'flex', gap: 12, cursor: 'pointer', borderColor: mode === 'auto' ? 'var(--accent)' : 'var(--border)', background: mode === 'auto' ? 'var(--accent-soft)' : 'var(--bg-2)' }} onClick={() => setMode('auto')}>
            <Icon name="zap" size={20} style={{ color: 'var(--accent)' }} />
            <div style={{ flex: 1 }}><div style={{ fontWeight: 600 }}>Авто</div><div className="muted" style={{ fontSize: 12 }}>Сделки мастера зеркалятся сразу по рынку. Быстро, но возможно проскальзывание.</div></div>
          </label>
          <label className="card" style={{ padding: '14px 16px', display: 'flex', gap: 12, cursor: 'pointer', borderColor: mode === 'manual' ? 'var(--accent)' : 'var(--border)', background: mode === 'manual' ? 'var(--accent-soft)' : 'var(--bg-2)' }} onClick={() => setMode('manual')}>
            <Icon name="bell" size={20} style={{ color: 'var(--accent)' }} />
            <div style={{ flex: 1 }}><div style={{ fontWeight: 600 }}>Вручную</div><div className="muted" style={{ fontSize: 12 }}>Приходит уведомление, ты сам подтверждаешь каждую копию.</div></div>
          </label>
        </div>
      )}

      {step === 3 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          <div className="label-cap">Как считать размер сделки</div>
          <div className="seg" style={{ width: '100%' }}>
            {[['pct_alloc', '% от аллокации'], ['proportional', 'Пропорция'], ['fixed', 'Фикс. USDT']].map(([k, l]) => (
              <button key={k} style={{ flex: 1 }} className={sizeMode === k ? 'on' : ''} onClick={() => setSizeMode(k)}>{l}</button>
            ))}
          </div>
          {sizeMode === 'pct_alloc' && (
            <>
              <div className="row" style={{ gap: 12 }}>
                <NumField style={{ flex: 1 }} label="Выделено (allocation)" value={allocation} onChange={setAllocation} suffix="USDT" />
                <NumField style={{ flex: 1 }} label="% на сделку" value={sizeValue} onChange={setSizeValue} suffix="%" hint="500$ × 2% = 10$ маржи" />
              </div>
            </>
          )}
          {sizeMode === 'proportional' && (
            <NumField label="Множитель к размеру мастера" value={sizeValue} onChange={setSizeValue} suffix="×" hint="×0.5 = половина размера мастера" />
          )}
          {sizeMode === 'fixed' && (
            <NumField label="Фиксированный размер" value={sizeValue} onChange={setSizeValue} suffix="USDT" hint="Одинаковый нотионал на каждую сделку" />
          )}
          <div className="card" style={{ padding: '10px 14px', background: 'var(--bg-2)', fontSize: 12.5 }}>
            Будет открываться: <b className="mono">{exampleSize}</b>
          </div>
          <NumField label="Макс. плечо (0 = как у мастера)" value={maxLev} onChange={setMaxLev} suffix="x" />
        </div>
      )}

      {step === 4 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div className="label-cap">Что делать с уже открытыми позициями мастера</div>
          <label className="card" style={{ padding: '14px 16px', display: 'flex', gap: 12, cursor: 'pointer', borderColor: copyExisting === 'new_only' ? 'var(--accent)' : 'var(--border)', background: copyExisting === 'new_only' ? 'var(--accent-soft)' : 'var(--bg-2)' }} onClick={() => setCopyExisting('new_only')}>
            <Icon name="plus" size={20} style={{ color: 'var(--accent)' }} />
            <div style={{ flex: 1 }}><div style={{ fontWeight: 600 }}>Только новые</div><div className="muted" style={{ fontSize: 12 }}>Копируем сделки, открытые после подписки. Текущие позиции мастера не трогаем.</div></div>
          </label>
          <label className="card" style={{ padding: '14px 16px', display: 'flex', gap: 12, cursor: 'pointer', borderColor: copyExisting === 'near_entry' ? 'var(--accent)' : 'var(--border)', background: copyExisting === 'near_entry' ? 'var(--accent-soft)' : 'var(--bg-2)' }} onClick={() => setCopyExisting('near_entry')}>
            <Icon name="target" size={20} style={{ color: 'var(--accent)' }} />
            <div style={{ flex: 1 }}><div style={{ fontWeight: 600 }}>Возле точки входа</div><div className="muted" style={{ fontSize: 12 }}>Копируем текущие позиции мастера, которые ещё возле ТВХ или в небольшом минусе (ROE ≤ +3%). Сильно прибыльные пропускаем — поздно входить.</div></div>
          </label>
          <div>
            <div className="label-cap" style={{ marginBottom: 6 }}>Авто-стоп подписки при убытке (0 = выкл)</div>
            <NumField value={stopLoss} onChange={setStopLoss} suffix="%" hint="Подписка остановится если суммарный убыток превысит этот %" />
          </div>
        </div>
      )}

      {step === 5 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {[
            ['Мастер', m.nickname],
            ['Биржа follower', exchange.toUpperCase()],
            ['Режим', mode === 'auto' ? 'Авто' : 'Вручную'],
            ['Размер', exampleSize],
            ['Плечо', maxLev === '0' ? 'как у мастера' : '×' + maxLev],
            ['Существующие', copyExisting === 'new_only' ? 'только новые' : 'возле ТВХ'],
            ['Стоп подписки', stopLoss === '0' ? 'выкл' : '-' + stopLoss + '%'],
            ['Комиссия мастера', (m.fee_percent || 10) + '% с прибыли'],
          ].map(([l, v]) => (
            <div key={l} className="row between" style={{ padding: '9px 0', borderBottom: '1px solid var(--border)' }}>
              <span className="muted" style={{ fontSize: 13 }}>{l}</span><span className="mono" style={{ fontWeight: 600, textAlign: 'right', maxWidth: '60%' }}>{v}</span>
            </div>
          ))}
        </div>
      )}
    </Modal>
  );
}

/* ---- Копи-трейдинг: уровни, спарклайн, лидерборд-категории ---- */
function LevelBadge({ tier_label, tier_color, sm }) {
  const c = tier_color || 'var(--text-muted)';
  return (
    <span className="pill" style={{ background: c + '20', color: c, border: `1px solid ${c}55`,
      fontWeight: 700, fontSize: sm ? 10 : 11.5, padding: sm ? '1px 7px' : '3px 9px', gap: 5 }}>
      <span className="dot" style={{ background: c }} />{tier_label || '—'}
    </span>
  );
}

function Sparkline({ data, height = 38, width = 130, strokeWidth = 1.6 }) {
  if (!data || data.length < 2) {
    return <div style={{ height, width, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <span className="muted" style={{ fontSize: 10 }}>нет данных</span></div>;
  }
  const min = Math.min(...data), max = Math.max(...data), range = (max - min) || 1;
  const pts = data.map((v, i) => {
    const x = (i / (data.length - 1)) * width;
    const y = height - ((v - min) / range) * (height - 4) - 2;
    return `${x.toFixed(1)},${y.toFixed(1)}`;
  });
  const up = data[data.length - 1] >= data[0];
  const col = up ? 'var(--green)' : 'var(--red)';
  const gid = 'spk' + Math.round(width) + (up ? 'u' : 'd');
  return (
    <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none" style={{ display: 'block' }}>
      <defs><linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stopColor={col} stopOpacity="0.22" /><stop offset="100%" stopColor={col} stopOpacity="0" />
      </linearGradient></defs>
      <polygon points={`0,${height} ${pts.join(' ')} ${width},${height}`} fill={`url(#${gid})`} stroke="none" />
      <polyline points={pts.join(' ')} fill="none" stroke={col} strokeWidth={strokeWidth} strokeLinejoin="round" strokeLinecap="round" />
    </svg>
  );
}

function fmtAgo(ms) {
  if (!ms) return '—';
  const s = Math.max(0, (Date.now() - ms) / 1000);
  if (s < 3600) return Math.max(1, Math.floor(s / 60)) + ' мин назад';
  if (s < 86400) return Math.floor(s / 3600) + ' ч назад';
  const d = Math.floor(s / 86400);
  return d + (d === 1 ? ' день' : ' дн') + ' назад';
}

const LB_LABEL = { roi: 'Доходность', pnl: 'Прибыль', win: 'Винрейт', followers: 'Подписчики' };
const LB_SORT = {
  roi: (a, b) => (b.roi_pct || 0) - (a.roi_pct || 0),
  pnl: (a, b) => (b.pnl_total || 0) - (a.pnl_total || 0),
  win: (a, b) => (b.win_rate || 0) - (a.win_rate || 0),
  followers: (a, b) => (b.followers || 0) - (a.followers || 0),
};
const LB_FMT = {
  roi: m => (m.roi_pct >= 0 ? '+' : '') + (m.roi_pct || 0) + '%',
  pnl: m => (m.pnl_total >= 0 ? '+' : '') + fmt.usdt(Math.abs(m.pnl_total || 0)),
  win: m => (m.win_rate || 0) + '%',
  followers: m => String(m.followers || 0),
};
const pnlColor = v => (v > 0 ? 'var(--green)' : v < 0 ? 'var(--red)' : 'var(--text)');

function MasterProfile({ m, onClose, onCopy, copied }) {
  const [detail, setDetail] = useState(null);
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    let on = true;
    (async () => {
      try {
        const r = await fetch(`/api/me/copytrade/masters/${m.master_id}`, { credentials: 'include' });
        if (on && r.ok) setDetail(await r.json());
      } catch {}
      if (on) setLoading(false);
    })();
    return () => { on = false; };
  }, [m.master_id]);
  const d = detail?.master || m;
  const hist = detail?.history || [];
  const riskMap = { low: 'низкий', medium: 'средний', high: 'высокий' };
  const Stat = ({ label, val, color }) => (
    <div className="card" style={{ padding: '11px 13px', background: 'var(--bg-2)' }}>
      <div className="label-cap" style={{ fontSize: 9.5 }}>{label}</div>
      <div className="mono" style={{ fontSize: 15.5, fontWeight: 700, marginTop: 4, color: color || 'var(--text)' }}>{val}</div>
    </div>
  );
  return (
    <Modal width={680} onClose={onClose}
      title={<div className="row" style={{ gap: 10 }}>
        <Avatar name={d.nickname} hue={(m.master_id * 47) % 360} size={34} />
        <div>
          <div className="row" style={{ gap: 8, alignItems: 'center' }}>{d.nickname}<LevelBadge tier_label={d.tier_label} tier_color={d.tier_color} sm /></div>
          <div className="muted" style={{ fontSize: 11.5, fontWeight: 400 }}>{d.followers || 0} подписчиков · {(d.exchange || 'weex').toUpperCase()} · риск {riskMap[d.risk_level] || d.risk_level}</div>
        </div>
      </div>}
      footer={<>
        <button className="btn btn-ghost" onClick={onClose}>Закрыть</button>
        <button className={cx('btn', copied ? 'btn-ghost' : 'btn-primary')} onClick={() => onCopy(m)}>{copied ? 'Управлять подпиской' : 'Копировать трейдера'}</button>
      </>}>
      {d.bio && <div className="muted" style={{ fontSize: 13, lineHeight: 1.5, marginBottom: 14 }}>{d.bio}</div>}
      <div className="card" style={{ padding: '12px 14px', background: 'var(--bg-2)', marginBottom: 14 }}>
        <div className="row between" style={{ marginBottom: 6 }}>
          <span className="label-cap">Кривая P&amp;L (последние сделки)</span>
          <span className="mono" style={{ fontWeight: 700, color: pnlColor(d.pnl_total) }}>{fmt.sign(d.pnl_total)}{fmt.usdt(Math.abs(d.pnl_total || 0))} USDT</span>
        </div>
        <Sparkline data={d.equity_curve} height={64} width={620} strokeWidth={2} />
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 10 }}>
        <Stat label="Доходность" val={(d.roi_pct >= 0 ? '+' : '') + (d.roi_pct || 0) + '%'} color={pnlColor(d.roi_pct)} />
        <Stat label="Винрейт" val={(d.win_rate || 0) + '%'} />
        <Stat label="Сделок" val={d.trades || 0} />
        <Stat label="PnL 30д" val={fmt.sign(d.pnl_30d) + fmt.usdt(Math.abs(d.pnl_30d || 0))} color={pnlColor(d.pnl_30d)} />
        <Stat label="Комиссия" val={(d.fee_percent || 10) + '%'} />
        <Stat label="Копи-объём" val={fmt.usdt(d.aum_usdt || 0)} />
        <Stat label="Ср. сделка" val={fmt.sign(d.avg_pnl) + fmt.usdt(Math.abs(d.avg_pnl || 0))} color={pnlColor(d.avg_pnl)} />
        <Stat label="Дней актив." val={d.days_active || 0} />
      </div>
      <div className="label-cap" style={{ margin: '16px 0 8px' }}>История сделок</div>
      {loading ? <div className="skel" style={{ height: 120, borderRadius: 10 }} /> :
        hist.length ? (
          <div className="card" style={{ overflow: 'hidden', maxHeight: 260, overflowY: 'auto' }}>
            <table className="tbl">
              <thead><tr><th>Пара</th><th>Сторона</th><th className="num">Вход</th><th className="num">Выход</th><th className="num">PnL</th><th className="num">Когда</th></tr></thead>
              <tbody>{hist.map((t, i) => (
                <tr key={i}>
                  <td className="mono strong">{t.symbol}</td>
                  <td><span className={cx('badge', t.direction === 'LONG' ? 'badge-long' : 'badge-short')}>{t.direction}{t.leverage ? ` ×${t.leverage}` : ''}</span></td>
                  <td className="num mono">{t.entry ?? '—'}</td>
                  <td className="num mono">{t.exit ?? '—'}</td>
                  <td className="num mono" style={{ color: pnlColor(t.pnl) }}>{t.pnl != null ? (fmt.sign(t.pnl) + fmt.usdt(Math.abs(t.pnl))) : '—'}{t.pnl_pct != null ? ` (${t.pnl_pct > 0 ? '+' : ''}${t.pnl_pct}%)` : ''}</td>
                  <td className="num muted" style={{ fontSize: 11.5 }}>{fmtAgo(t.closed_ms)}</td>
                </tr>
              ))}</tbody>
            </table>
          </div>
        ) : <div className="muted" style={{ fontSize: 12.5, padding: '8px 2px' }}>Закрытых сделок пока нет — статистика появится после первых закрытий.</div>
      }
    </Modal>
  );
}

function SubscriptionDetail({ sub, onClose, patchSub, onEdit }) {
  const [trades, setTrades] = useState(null);
  useEffect(() => {
    let on = true;
    (async () => {
      try {
        const r = await fetch(`/api/me/copytrade/trades?sub_id=${sub.id}`, { credentials: 'include' });
        if (on && r.ok) { const d = await r.json(); setTrades(d.trades || []); return; }
      } catch {}
      if (on) setTrades([]);
    })();
    return () => { on = false; };
  }, [sub.id]);
  const sizeStr = sub.size_mode === 'pct_alloc' ? `${sub.size_value}% от ${fmt.usdt(sub.allocation_usdt, 0)} (маржа)`
    : sub.size_mode === 'fixed' ? `${sub.size_value} USDT (нотионал)` : `×${sub.size_value} от размера мастера`;
  const created = sub.created_at ? new Date(typeof sub.created_at === 'number' ? sub.created_at : Date.parse(sub.created_at)) : null;
  const Stat = ({ label, val, color }) => (
    <div className="card" style={{ padding: '10px 12px', background: 'var(--bg-2)' }}>
      <div className="label-cap" style={{ fontSize: 9 }}>{label}</div>
      <div className="mono" style={{ fontSize: 15, fontWeight: 700, marginTop: 3, color: color || 'var(--text)' }}>{val}</div>
    </div>
  );
  const Row = ({ l, v }) => (
    <div className="row between" style={{ padding: '8px 0', borderBottom: '1px solid var(--border)' }}>
      <span className="muted" style={{ fontSize: 12.5 }}>{l}</span><span className="mono" style={{ fontWeight: 600, textAlign: 'right' }}>{v}</span>
    </div>
  );
  return (
    <Modal width={640} onClose={onClose}
      title={<div className="row" style={{ gap: 10 }}>
        <Avatar name={sub.master_nickname} hue={(sub.master_id * 47) % 360} size={32} />
        <div><div className="row" style={{ gap: 8, alignItems: 'center' }}>{sub.master_nickname}<StatusPill status={sub.status} /></div>
          <div className="muted" style={{ fontSize: 11.5, fontWeight: 400 }}>подписка #{sub.id} · {sub.follower_exchange.toUpperCase()}</div></div>
      </div>}
      footer={<>
        <button className="btn btn-ghost" onClick={onClose}>Закрыть</button>
        {sub.status !== 'stopped' && <button className="btn btn-ghost" onClick={() => onEdit(sub)}>Изменить</button>}
        {sub.status === 'active' && <button className="btn btn-ghost" onClick={() => { patchSub(sub.id, { status: 'paused' }, 'Пауза'); onClose(); }}>Пауза</button>}
        {sub.status === 'paused' && <button className="btn btn-ghost" onClick={() => { patchSub(sub.id, { status: 'active' }, 'Возобновлено'); onClose(); }}>Возобновить</button>}
        {sub.status !== 'stopped' && <button className="btn btn-ghost" style={{ color: 'var(--red)' }} onClick={() => { patchSub(sub.id, null, 'Подписка остановлена'); onClose(); }}>Стоп</button>}
      </>}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 10, marginBottom: 14 }}>
        <Stat label="Скопировано" val={(sub.copied_trades || 0) + ' сд.'} />
        <Stat label="Итог P&L" val={fmt.sign(sub.realized_pnl) + fmt.usdt(Math.abs(sub.realized_pnl || 0))} color={pnlColor(sub.realized_pnl)} />
        <Stat label="Комиссия" val={fmt.usdt(sub.fees_paid || 0)} />
        <Stat label="Режим" val={sub.mode === 'auto' ? 'Авто' : 'Вручную'} />
      </div>
      <div className="label-cap" style={{ marginBottom: 4 }}>Настройки подписки</div>
      <div style={{ marginBottom: 14 }}>
        <Row l="Размер копии" v={sizeStr} />
        <Row l="Плечо" v={sub.max_leverage ? '×' + sub.max_leverage : 'как у мастера'} />
        <Row l="Старые позиции" v={sub.copy_existing === 'near_entry' ? 'копировать возле ТВХ' : 'только новые'} />
        <Row l="Авто-стоп при убытке" v={sub.stop_loss_pct ? '-' + sub.stop_loss_pct + '%' : 'выкл'} />
        {created && <Row l="Создана" v={created.toLocaleDateString('ru-RU') + ' ' + created.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })} />}
      </div>
      <div className="label-cap" style={{ marginBottom: 6 }}>Скопированные сделки {trades ? `(${trades.length})` : ''}</div>
      {trades === null ? <div className="skel" style={{ height: 90, borderRadius: 10 }} /> :
        trades.length ? (
          <div className="card" style={{ overflow: 'hidden', maxHeight: 240, overflowY: 'auto' }}>
            <table className="tbl">
              <thead><tr><th>Пара</th><th>Сторона</th><th className="num">Размер</th><th>Статус</th><th className="num">P&L</th></tr></thead>
              <tbody>{trades.map((t, i) => (
                <tr key={i}>
                  <td className="mono strong">{t.symbol}</td>
                  <td><span className={cx('badge', t.direction === 'LONG' ? 'badge-long' : 'badge-short')}>{t.direction}</span></td>
                  <td className="num mono">{fmt.usdt(t.size_usdt || 0)}</td>
                  <td><StatusPill status={t.status === 'open' ? 'open' : t.status === 'failed' ? 'failed' : 'closed'} /></td>
                  <td className="num mono" style={{ color: pnlColor(t.pnl_usdt) }}>{t.pnl_usdt ? (fmt.sign(t.pnl_usdt) + fmt.usdt(Math.abs(t.pnl_usdt))) : (t.status === 'failed' ? '—' : '0.00')}</td>
                </tr>
              ))}</tbody>
            </table>
          </div>
        ) : <div className="muted" style={{ fontSize: 12.5, padding: '6px 2px' }}>Сделок по этой подписке пока нет.</div>
      }
    </Modal>
  );
}

function SubscriptionEdit({ sub, onClose, onSaved }) {
  const [mode, setMode] = useState(sub.mode || 'auto');
  const [sizeMode, setSizeMode] = useState(sub.size_mode || 'pct_alloc');
  const [sizeValue, setSizeValue] = useState(String(sub.size_value ?? 1));
  const [allocation, setAllocation] = useState(String(sub.allocation_usdt ?? 100));
  const [maxLev, setMaxLev] = useState(String(sub.max_leverage ?? 0));
  const [stopLoss, setStopLoss] = useState(String(sub.stop_loss_pct ?? 0));
  const [busy, setBusy] = useState(false);
  const toast = useToast();
  const sizeHint = sizeMode === 'pct_alloc' ? `${(parseFloat(allocation) * parseFloat(sizeValue) / 100 || 0).toFixed(2)} USDT маржи (${sizeValue}% от ${allocation}) × плечо`
    : sizeMode === 'fixed' ? `${sizeValue} USDT нотионала на сделку` : `×${sizeValue} от размера мастера`;
  const save = async () => {
    setBusy(true);
    try {
      const body = {
        mode, size_mode: sizeMode, size_value: parseFloat(sizeValue) || 1,
        allocation_usdt: parseFloat(allocation) || 0, max_leverage: parseInt(maxLev) || 0,
        stop_loss_pct: parseFloat(stopLoss) || 0,
      };
      const r = await fetch(`/api/me/copytrade/subscriptions/${sub.id}`, {
        method: 'PATCH', credentials: 'include',
        headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
      });
      if (!r.ok) throw new Error((await r.json().catch(() => ({}))).detail || r.status);
      toast('Сохранено — применится к новым сделкам', 'success');
      onSaved();
    } catch (e) { toast('Ошибка: ' + (e.message || e), 'error'); }
    setBusy(false);
  };
  return (
    <Modal width={560} onClose={onClose} title={`Настройки копирования · ${sub.master_nickname}`}
      footer={<>
        <button className="btn btn-ghost" onClick={onClose}>Отмена</button>
        <button className="btn btn-primary" disabled={busy} onClick={save}>{busy ? 'Сохраняем…' : 'Сохранить'}</button>
      </>}>
      <div className="card" style={{ padding: '10px 14px', background: 'var(--amber-soft)', border: '1px solid var(--amber)', fontSize: 12.5, marginBottom: 14 }}>
        Изменения применятся <b>только к новым сделкам</b>. Уже открытые копии не трогаем — они доводятся и закрываются по позиции мастера как есть.
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
        <div><div className="label-cap" style={{ marginBottom: 6 }}>Режим</div>
          <div className="seg" style={{ width: '100%' }}>{[['auto', 'Авто'], ['manual', 'Вручную']].map(([k, l]) => <button key={k} style={{ flex: 1 }} className={mode === k ? 'on' : ''} onClick={() => setMode(k)}>{l}</button>)}</div></div>
        <div><div className="label-cap" style={{ marginBottom: 6 }}>Как считать размер</div>
          <div className="seg" style={{ width: '100%' }}>{[['pct_alloc', '% от аллокации'], ['proportional', 'Пропорция'], ['fixed', 'Фикс. USDT']].map(([k, l]) => <button key={k} style={{ flex: 1 }} className={sizeMode === k ? 'on' : ''} onClick={() => setSizeMode(k)}>{l}</button>)}</div></div>
        {sizeMode === 'pct_alloc' && <div className="row" style={{ gap: 12 }}>
          <NumField style={{ flex: 1 }} label="Выделено (allocation)" value={allocation} onChange={setAllocation} suffix="USDT" />
          <NumField style={{ flex: 1 }} label="% на сделку" value={sizeValue} onChange={setSizeValue} suffix="%" hint="это маржа, не нотионал" />
        </div>}
        {sizeMode === 'proportional' && <NumField label="Множитель к размеру мастера" value={sizeValue} onChange={setSizeValue} suffix="×" hint="×0.5 = половина размера мастера" />}
        {sizeMode === 'fixed' && <NumField label="Фиксированный размер" value={sizeValue} onChange={setSizeValue} suffix="USDT" hint="нотионал на каждую сделку" />}
        <div className="row" style={{ gap: 12 }}>
          <NumField style={{ flex: 1 }} label="Макс. плечо (0 = как у мастера)" value={maxLev} onChange={setMaxLev} suffix="x" />
          <NumField style={{ flex: 1 }} label="Авто-стоп при убытке (0=выкл)" value={stopLoss} onChange={setStopLoss} suffix="%" />
        </div>
        <div className="card" style={{ padding: '10px 14px', background: 'var(--bg-2)', fontSize: 12.5 }}>Новые сделки: <b className="mono">{sizeHint}</b></div>
      </div>
    </Modal>
  );
}

function MasterCardLive({ m, copied, onOpen, onCopy }) {
  const riskColor = { low: 'var(--green)', medium: 'var(--amber)', high: 'var(--red)' }[m.risk_level] || 'var(--text-muted)';
  const roiCol = pnlColor(m.roi_pct);
  return (
    <div className="card card-pad fade-in" style={{ display: 'flex', flexDirection: 'column', gap: 12, cursor: 'pointer' }} onClick={() => onOpen(m)}>
      <div className="row between">
        <div className="row" style={{ gap: 12 }}>
          <Avatar name={m.nickname} hue={(m.master_id * 47) % 360} size={44} />
          <div>
            <div className="row" style={{ gap: 7, alignItems: 'center' }}><span style={{ fontWeight: 700, fontSize: 14.5 }}>{m.nickname}</span><LevelBadge tier_label={m.tier_label} tier_color={m.tier_color} sm /></div>
            <div className="muted" style={{ fontSize: 11.5, marginTop: 3 }}>{m.followers || 0} подписчиков · {(m.exchange || 'weex').toUpperCase()}</div>
          </div>
        </div>
        {copied && <span className="badge badge-emerald">Подписан</span>}
      </div>
      <div className="row between" style={{ gap: 10, alignItems: 'flex-end' }}>
        <div className="row" style={{ gap: 16 }}>
          <div>
            <div className="label-cap" style={{ fontSize: 9 }}>Доходность</div>
            <div className="mono" style={{ fontSize: 16, fontWeight: 700, color: roiCol }}>{(m.roi_pct >= 0 ? '+' : '')}{m.roi_pct || 0}%</div>
          </div>
          <div>
            <div className="label-cap" style={{ fontSize: 9 }}>Винрейт</div>
            <div className="mono" style={{ fontSize: 16, fontWeight: 700 }}>{m.win_rate || 0}%</div>
          </div>
          <div>
            <div className="label-cap" style={{ fontSize: 9 }}>Сделок</div>
            <div className="mono" style={{ fontSize: 16, fontWeight: 700 }}>{m.trades || 0}</div>
          </div>
        </div>
        <Sparkline data={m.equity_curve} height={40} width={96} />
      </div>
      {m.bio && <div className="muted" style={{ fontSize: 12, lineHeight: 1.4, minHeight: 17 }}>{m.bio.slice(0, 78)}</div>}
      <div className="row" style={{ gap: 8 }}>
        <span className="pill pill-soft" style={{ color: riskColor }}><span className="dot" style={{ background: riskColor }} />{{ low: 'низкий риск', medium: 'средний риск', high: 'высокий риск' }[m.risk_level] || 'риск ?'}</span>
        <span className="pill pill-soft">комиссия {m.fee_percent || 10}%</span>
      </div>
      <div className="divider" />
      <div className="row between">
        <span className="muted" style={{ fontSize: 11.5 }}>Копи-объём {fmt.usdt(m.aum_usdt || 0)}</span>
        <button className={cx('btn btn-sm', copied ? 'btn-ghost' : 'btn-primary')} onClick={e => { e.stopPropagation(); onCopy(m); }}>
          {copied ? 'Управлять' : 'Копировать'}
        </button>
      </div>
    </div>
  );
}

function BecomeMasterPane({ onChanged }) {
  const [p, loading] = useLiveData(
    async () => { const r = await fetch('/api/me/copytrade/me', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, []
  );
  const [nickname, setNickname] = useState('');
  const [bio, setBio] = useState('');
  const [exchange, setExchange] = useState('weex');
  const [fee, setFee] = useState('10');
  const [risk, setRisk] = useState('medium');
  const [busy, setBusy] = useState(false);
  const toast = useToast();
  const existing = p?.profile;
  useEffect(() => {
    if (existing) {
      setNickname(existing.nickname || ''); setBio(existing.bio || '');
      setExchange(existing.exchange || 'weex'); setFee(String(existing.fee_percent || 10));
      setRisk(existing.risk_level || 'medium');
    }
  }, [existing]);

  const save = async () => {
    if (!nickname.trim()) { toast('Укажи никнейм', 'error'); return; }
    setBusy(true);
    try {
      const r = await fetch('/api/me/copytrade/me', {
        method: 'PUT', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ nickname: nickname.trim(), bio: bio.trim(), exchange, fee_percent: parseFloat(fee) || 10, risk_level: risk, is_public: true }),
      });
      if (!r.ok) throw new Error((await r.json().catch(() => ({}))).detail || r.status);
      toast(existing ? 'Профиль обновлён' : 'Ты стал мастером — профиль опубликован', 'success');
      onChanged && onChanged();
    } catch (e) { toast('Не удалось: ' + (e.message || e), 'error'); }
    setBusy(false);
  };
  const unpublish = async () => {
    if (!confirm('Снять профиль мастера с публикации? Подписчики перестанут копировать.')) return;
    setBusy(true);
    try {
      await fetch('/api/me/copytrade/me', { method: 'DELETE', credentials: 'include' });
      toast('Профиль снят с публикации', 'info'); onChanged && onChanged();
    } catch (e) { toast('Ошибка', 'error'); }
    setBusy(false);
  };

  if (loading) return <div className="skel" style={{ height: 300, borderRadius: 12 }} />;
  return (
    <div className="card card-pad" style={{ maxWidth: 620, margin: '0 auto', width: '100%' }}>
      <div className="row" style={{ gap: 12, marginBottom: 16 }}>
        <div style={{ display: 'inline-flex', padding: 12, borderRadius: 14, background: 'var(--accent-soft)', color: 'var(--accent)' }}><Icon name="crown" size={24} /></div>
        <div>
          <div style={{ fontWeight: 700, fontSize: 17 }}>{existing ? 'Профиль мастера' : 'Стать мастер-трейдером'}</div>
          <div className="muted" style={{ fontSize: 12.5 }}>{existing ? `${p.followers} активных подписчиков` : 'Публикуй свои сделки — другие будут их копировать'}</div>
        </div>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
        <div><div className="label-cap" style={{ marginBottom: 6 }}>Никнейм</div><input className="inp" value={nickname} onChange={e => setNickname(e.target.value)} placeholder="Как тебя будут видеть" /></div>
        <div><div className="label-cap" style={{ marginBottom: 6 }}>Описание стратегии</div><textarea className="inp" style={{ minHeight: 70, padding: 10 }} value={bio} onChange={e => setBio(e.target.value)} placeholder="Интрадей по альтам, жёсткий риск-менеджмент…" /></div>
        <div className="row" style={{ gap: 12 }}>
          <div style={{ flex: 1 }}>
            <div className="label-cap" style={{ marginBottom: 6 }}>Биржа публикации</div>
            <div className="seg" style={{ width: '100%' }}>{['weex', 'okx', 'binance', 'bybit'].map(e => <button key={e} style={{ flex: 1 }} className={exchange === e ? 'on' : ''} onClick={() => setExchange(e)}>{e.toUpperCase()}</button>)}</div>
          </div>
        </div>
        <div className="row" style={{ gap: 12 }}>
          <NumField style={{ flex: 1 }} label="Комиссия с прибыли" value={fee} onChange={setFee} suffix="%" hint="0–50%" />
          <div style={{ flex: 1 }}>
            <div className="label-cap" style={{ marginBottom: 6 }}>Уровень риска</div>
            <div className="seg" style={{ width: '100%' }}>{[['low', 'Низк'], ['medium', 'Сред'], ['high', 'Выс']].map(([k, l]) => <button key={k} style={{ flex: 1 }} className={risk === k ? 'on' : ''} onClick={() => setRisk(k)}>{l}</button>)}</div>
          </div>
        </div>
        <div className="row" style={{ gap: 10, marginTop: 4 }}>
          <button className="btn btn-primary" disabled={busy} onClick={save}>{busy ? 'Сохраняем…' : (existing ? 'Обновить профиль' : 'Опубликовать профиль')}</button>
          {existing && <button className="btn btn-ghost" style={{ color: 'var(--red)' }} disabled={busy} onClick={unpublish}>Снять с публикации</button>}
        </div>
      </div>
    </div>
  );
}

function CopyTrade() {
  const [tab, setTab] = useState('market');
  const [risk, setRisk] = useState('all');
  const [lbCat, setLbCat] = useState('roi');
  const [profile, setProfile] = useState(null);
  const [subDetail, setSubDetail] = useState(null);
  const [subEdit, setSubEdit] = useState(null);
  const [subscribe, setSubscribe] = useState(null);
  const [tick, setTick] = useState(0);
  const reload = () => setTick(t => t + 1);
  const toast = useToast();

  const [mastersResp, mLoading] = useLiveData(
    async () => { const r = await fetch('/api/me/copytrade/masters', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, [tick]
  );
  const [subsResp] = useLiveData(
    async () => { const r = await fetch('/api/me/copytrade/subscriptions', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, [tick]
  );
  const [credsRaw] = useLiveData(
    async () => { const r = await fetch('/api/me/exchange-creds', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, []
  );
  const accounts = useMemo(() => {
    const seen = new Set();
    return (credsRaw || []).filter(c => { const e = (c.exchange || '').toLowerCase(); if (seen.has(e)) return false; seen.add(e); return true; })
      .map(c => ({ id: (c.exchange || '').toLowerCase(), name: (c.exchange || '').toUpperCase(), key: c.api_key_mask || '••••' }));
  }, [credsRaw]);

  const masters = mastersResp?.masters || [];
  const subs = subsResp?.subscriptions || [];
  const activeSubs = subs.filter(s => s.status === 'active');
  const workingSubs = subs.filter(s => s.status === 'active' || s.status === 'paused');
  const historySubs = subs.filter(s => s.status === 'stopped');
  const mineIds = new Set(activeSubs.map(s => s.master_id));
  const filtered = masters.filter(m => risk === 'all' || m.risk_level === risk);

  const doCopy = (m) => { if (mineIds.has(m.master_id)) setTab('mine'); else setSubscribe(m); };

  const patchSub = async (id, body, okMsg) => {
    try {
      const r = await fetch(`/api/me/copytrade/subscriptions/${id}`, {
        method: body === null ? 'DELETE' : 'PATCH', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: body === null ? undefined : JSON.stringify(body),
      });
      if (!r.ok) throw new Error(r.status);
      toast(okMsg, 'success'); reload();
    } catch (e) { toast('Ошибка: ' + (e.message || e), 'error'); }
  };

  return (
    <div className="fade-in" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 18 }}>
      <div className="tabs" style={{ border: 'none' }}>
        {[['market', 'Маркетплейс'], ['mine', `Мои подписки${activeSubs.length ? ' (' + activeSubs.length + ')' : ''}`], ['leaderboard', 'Лидерборд'], ['become', 'Стать мастером']].map(([k, l]) => (
          <button key={k} className={tab === k ? 'on' : ''} onClick={() => setTab(k)}>{l}</button>
        ))}
      </div>

      {tab === 'market' && <>
        <div className="row between wrap" style={{ gap: 12 }}>
          <div className="seg">{[['all', 'Все'], ['low', 'Низкий риск'], ['medium', 'Средний'], ['high', 'Высокий']].map(([k, l]) => <button key={k} className={risk === k ? 'on' : ''} onClick={() => setRisk(k)}>{l}</button>)}</div>
          <span className="muted" style={{ fontSize: 12 }}>{masters.length} мастеров</span>
        </div>
        {mLoading && !masters.length ? <div className="skel" style={{ height: 160, borderRadius: 12 }} /> :
          filtered.length ? (
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: 16 }}>
              {filtered.map(m => <MasterCardLive key={m.master_id} m={m} copied={mineIds.has(m.master_id)} onOpen={setProfile} onCopy={doCopy} />)}
            </div>
          ) : <div className="card"><Empty icon="copytrade" title="Пока нет публичных мастеров" text="Стань первым — вкладка «Стать мастером»." action={<button className="btn btn-primary" onClick={() => setTab('become')}>Стать мастером</button>} /></div>
        }
      </>}

      {tab === 'mine' && (
        workingSubs.length || historySubs.length ? (
          <>
            {/* Активные / на паузе — рабочие подписки */}
            {workingSubs.length ? (
              <div className="card" style={{ overflow: 'hidden' }}>
                <table className="tbl">
                  <thead><tr><th>Мастер</th><th>Биржа</th><th>Режим</th><th>Размер</th><th className="num">Скопир.</th><th className="num">P&L</th><th>Статус</th><th></th></tr></thead>
                  <tbody>{workingSubs.map(s => (
                    <tr key={s.id} className="clickable" onClick={() => setSubDetail(s)}>
                      <td><div className="row" style={{ gap: 10 }}><Avatar name={s.master_nickname} hue={(s.master_id * 47) % 360} size={28} /><span className="strong">{s.master_nickname}</span></div></td>
                      <td className="mono">{s.follower_exchange.toUpperCase()}</td>
                      <td><span className="pill pill-soft">{s.mode === 'auto' ? 'Авто' : 'Вручную'}</span></td>
                      <td className="mono" style={{ fontSize: 12 }}>{s.size_mode === 'pct_alloc' ? `${s.size_value}% от ${fmt.usdt(s.allocation_usdt, 0)}` : s.size_mode === 'fixed' ? `${s.size_value} USDT` : `×${s.size_value}`}</td>
                      <td className="num mono">{s.copied_trades || 0}</td>
                      <td className={cx('num mono', pn(s.realized_pnl))}>{fmt.sign(s.realized_pnl)}{fmt.usdt(s.realized_pnl)}</td>
                      <td><StatusPill status={s.status} /></td>
                      <td className="num">
                        <div className="row" style={{ gap: 6, justifyContent: 'flex-end' }} onClick={e => e.stopPropagation()}>
                          <button className="btn btn-ghost btn-sm" onClick={() => setSubEdit(s)}>Изменить</button>
                          {s.status === 'active'
                            ? <button className="btn btn-ghost btn-sm" onClick={() => patchSub(s.id, { status: 'paused' }, 'Пауза')}>Пауза</button>
                            : <button className="btn btn-ghost btn-sm" onClick={() => patchSub(s.id, { status: 'active' }, 'Возобновлено')}>Старт</button>}
                          <button className="btn btn-ghost btn-sm" style={{ color: 'var(--red)' }} onClick={() => patchSub(s.id, null, 'Подписка остановлена')}>Стоп</button>
                        </div>
                      </td>
                    </tr>
                  ))}</tbody>
                </table>
              </div>
            ) : <div className="card"><Empty icon="copytrade" title="Нет активных подписок" text="Выберите трейдера в маркетплейсе." action={<button className="btn btn-primary" onClick={() => setTab('market')}>В маркетплейс</button>} /></div>}

            {/* История — остановленные подписки со статистикой */}
            {historySubs.length > 0 && <>
              <div className="label-cap" style={{ marginTop: 6 }}>История подписок ({historySubs.length})</div>
              <div className="card" style={{ overflow: 'hidden' }}>
                <table className="tbl">
                  <thead><tr><th>Мастер</th><th>Биржа</th><th>Размер</th><th className="num">Скопировано</th><th className="num">Итог P&L</th><th>Статус</th><th></th></tr></thead>
                  <tbody>{historySubs.map(s => (
                    <tr key={s.id} className="clickable" onClick={() => setSubDetail(s)}>
                      <td><div className="row" style={{ gap: 10 }}><Avatar name={s.master_nickname} hue={(s.master_id * 47) % 360} size={26} /><span className="strong">{s.master_nickname}</span></div></td>
                      <td className="mono">{s.follower_exchange.toUpperCase()}</td>
                      <td className="mono" style={{ fontSize: 12 }}>{s.size_mode === 'pct_alloc' ? `${s.size_value}% от ${fmt.usdt(s.allocation_usdt, 0)}` : s.size_mode === 'fixed' ? `${s.size_value} USDT` : `×${s.size_value}`}</td>
                      <td className="num mono">{s.copied_trades || 0} сделок</td>
                      <td className={cx('num mono', pn(s.realized_pnl))}>{fmt.sign(s.realized_pnl)}{fmt.usdt(s.realized_pnl)}</td>
                      <td><StatusPill status={s.status} /></td>
                      <td className="num" onClick={e => e.stopPropagation()}>
                        <button className="btn btn-ghost btn-sm" onClick={() => setSubscribe(masters.find(m => m.master_id === s.master_id) || { master_id: s.master_id, nickname: s.master_nickname })}>Возобновить</button>
                      </td>
                    </tr>
                  ))}</tbody>
                </table>
              </div>
            </>}
          </>
        ) : <div className="card"><Empty icon="copytrade" title="Вы пока никого не копируете" text="Выберите трейдера в маркетплейсе." action={<button className="btn btn-primary" onClick={() => setTab('market')}>В маркетплейс</button>} /></div>
      )}

      {tab === 'leaderboard' && (
        masters.length ? (
          <>
            <div className="row between wrap" style={{ gap: 12 }}>
              <div className="seg">{[['roi', 'Доходность'], ['pnl', 'Прибыль'], ['win', 'Винрейт'], ['followers', 'Подписчики']].map(([k, l]) => <button key={k} className={lbCat === k ? 'on' : ''} onClick={() => setLbCat(k)}>{l}</button>)}</div>
              <span className="muted" style={{ fontSize: 12 }}>топ по: {LB_LABEL[lbCat]}</span>
            </div>
            <div className="card" style={{ overflow: 'hidden' }}>
              <table className="tbl">
                <thead><tr><th style={{ width: 48 }}>#</th><th>Трейдер</th><th>Уровень</th><th className="num">{LB_LABEL[lbCat]}</th><th className="num">Винрейт</th><th className="num">Сделок</th><th></th></tr></thead>
                <tbody>{[...masters].sort(LB_SORT[lbCat]).map((m, i) => {
                  const metricCol = (lbCat === 'roi' || lbCat === 'pnl') ? pnlColor(lbCat === 'roi' ? m.roi_pct : m.pnl_total) : 'var(--text)';
                  const rankCol = ['#f7c948', '#cbd5e1', '#d97706'][i] || 'var(--text-muted)';
                  return (
                    <tr key={m.master_id} className="clickable" onClick={() => setProfile(m)}>
                      <td><span className="mono" style={{ fontWeight: 800, fontSize: i < 3 ? 15 : 13, color: rankCol }}>{i + 1}</span></td>
                      <td><div className="row" style={{ gap: 10 }}><Avatar name={m.nickname} hue={(m.master_id * 47) % 360} size={30} /><span className="strong">{m.nickname}</span></div></td>
                      <td><LevelBadge tier_label={m.tier_label} tier_color={m.tier_color} sm /></td>
                      <td className="num mono" style={{ fontWeight: 700, color: metricCol }}>{LB_FMT[lbCat](m)}</td>
                      <td className="num mono">{m.win_rate || 0}%</td>
                      <td className="num mono">{m.trades || 0}</td>
                      <td className="num"><button className="btn btn-primary btn-sm" onClick={e => { e.stopPropagation(); doCopy(m); }}>{mineIds.has(m.master_id) ? '✓' : 'Копировать'}</button></td>
                    </tr>
                  );
                })}</tbody>
              </table>
            </div>
          </>
        ) : <div className="card"><Empty icon="trophy" title="Лидерборд пуст" text="Мастеров пока нет." /></div>
      )}

      {tab === 'become' && <BecomeMasterPane onChanged={reload} />}

      {profile && <MasterProfile m={profile} copied={mineIds.has(profile.master_id)} onClose={() => setProfile(null)} onCopy={(m) => { setProfile(null); doCopy(m); }} />}
      {subDetail && <SubscriptionDetail sub={subDetail} onClose={() => setSubDetail(null)} patchSub={patchSub} onEdit={(s) => { setSubDetail(null); setSubEdit(s); }} />}
      {subEdit && <SubscriptionEdit sub={subEdit} onClose={() => setSubEdit(null)} onSaved={() => { setSubEdit(null); reload(); }} />}
      {subscribe && <CopySubscribeModal m={subscribe} accounts={accounts} onClose={() => setSubscribe(null)} onDone={() => { setSubscribe(null); reload(); setTab('mine'); }} />}
    </div>
  );
}

/* ===================== SIGNALS (3 sub-tabs) ============================= */
function SignalCard({ s }) {
  return (
    <div className="card card-pad fade-in" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <div className="row between">
        <div className="row" style={{ gap: 10 }}>
          <span className={cx('badge', s.side === 'long' ? 'badge-long' : 'badge-short')}>{s.side.toUpperCase()}</span>
          <span className="strong mono" style={{ fontSize: 15 }}>{s.symbol}</span>
          <span className="muted mono" style={{ fontSize: 12 }}>×{s.lev}</span>
        </div>
        <StatusPill status={s.status} />
      </div>
      <div className="row" style={{ gap: 8 }}><span style={{ color: s.accent }}><Icon name="signal" size={14} /></span><span className="muted" style={{ fontSize: 12.5 }}>{s.source} · {s.ago}</span></div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 8 }}>
        <div className="card" style={{ padding: '8px 10px', background: 'var(--bg-2)' }}><div className="label-cap" style={{ fontSize: 9 }}>Вход</div><div className="mono" style={{ fontSize: 13, fontWeight: 600 }}>{s.entry}</div></div>
        <div className="card" style={{ padding: '8px 10px', background: 'var(--bg-2)' }}><div className="label-cap" style={{ fontSize: 9 }}>TP1</div><div className="mono pos" style={{ fontSize: 13, fontWeight: 600 }}>{s.tps[0]}</div></div>
        <div className="card" style={{ padding: '8px 10px', background: 'var(--bg-2)' }}><div className="label-cap" style={{ fontSize: 9 }}>SL</div><div className="mono neg" style={{ fontSize: 13, fontWeight: 600 }}>{s.sl}</div></div>
      </div>
    </div>
  );
}

function ChannelSettingsModal({ c, onClose, onSaved, onDeleted }) {
  // Бэк хранит per-channel: entry_type (market|limit|signal) + default_sl_pct.
  const [entryType, setEntryType] = useState((c.entry_type || 'signal').toLowerCase());
  const [slPct, setSlPct] = useState(String(c.sl_pct ?? 0));
  const [label, setLabel] = useState(c.label || '');
  const [busy, setBusy] = useState(false);
  const [bt, setBt] = useState(null);      // результат бэктеста
  const [btBusy, setBtBusy] = useState(false);
  const pollRef = useRef(null);
  const toast = useToast();

  // Опрос результата фоновой задачи (бэктест может считаться минуты)
  const pollResult = () => {
    let tries = 0;
    clearInterval(pollRef.current);
    pollRef.current = setInterval(async () => {
      tries++;
      try {
        const r = await fetch(`/api/me/signal-channels/${c.id}/backtest-result`, { credentials: 'include' });
        const j = await r.json();
        if (j.status === 'done') {
          if (j.result) setBt(j.result);
          setBtBusy(false); clearInterval(pollRef.current);
        } else if (j.status === 'failed') {
          if (j.result) setBt(j.result);  // покажем причину и в окне, не только тостом
          toast('Бэктест не удался' + (j.result && j.result.error ? ': ' + j.result.error : ''), 'error');
          setBtBusy(false); clearInterval(pollRef.current);
        }
      } catch (e) { /* транзиентная сеть — продолжаем опрос */ }
      if (tries > 90) { setBtBusy(false); clearInterval(pollRef.current); }  // ~7.5 мин кап
    }, 5000);
  };

  // При открытии модалки — подтянуть прошлый результат / возобновить опрос
  useEffect(() => {
    (async () => {
      try {
        const r = await fetch(`/api/me/signal-channels/${c.id}/backtest-result`, { credentials: 'include' });
        const j = await r.json();
        if (j.result) setBt(j.result);
        if (j.status === 'running') { setBtBusy(true); pollResult(); }
      } catch (e) { /* нет результата — норм */ }
    })();
    return () => clearInterval(pollRef.current);
  }, []);

  const runBacktest = async () => {
    setBtBusy(true); setBt(null);
    try {
      const r = await fetch(`/api/me/signal-channels/${c.id}/backtest?limit=50`, { method: 'POST', credentials: 'include' });
      const j = await r.json();
      if (!r.ok || j.ok === false) throw new Error(j.error || j.detail || 'ошибка');
      toast(j.message || 'Бэктест запущен — результат придёт в бота и на сайт', 'info');
      pollResult();
    } catch (e) {
      toast('Бэктест не удался: ' + (e.message || e), 'error');
      setBtBusy(false);
    }
  };

  const save = async () => {
    setBusy(true);
    try {
      await API().updateChannel(c.id, {
        label: label.trim() || undefined,
        default_entry_type: entryType,
        default_sl_pct: parseFloat(slPct) || 0,
      });
      toast('Настройки канала сохранены', 'success');
      onSaved && onSaved(); onClose();
    } catch (e) { toast('Не удалось: ' + (e.message || e), 'error'); }
    setBusy(false);
  };
  const del = async () => {
    if (!confirm(`Удалить канал ${c.name}? Сигналы из него перестанут приходить.`)) return;
    setBusy(true);
    try {
      await API().removeChannel(c.id);
      toast('Канал удалён', 'info');
      onDeleted && onDeleted(); onClose();
    } catch (e) { toast('Не удалось удалить: ' + (e.message || e), 'error'); }
    setBusy(false);
  };

  return (
    <Modal title={`Настройки · ${c.name}`} sub={c.tg} onClose={onClose} width={520}
      footer={<>
        <button className="btn btn-ghost" style={{ color: 'var(--red)', marginRight: 'auto' }} disabled={busy} onClick={del}>
          <Icon name="trash" size={14} />Удалить канал
        </button>
        <button className="btn btn-ghost" disabled={busy} onClick={onClose}>Отмена</button>
        <button className="btn btn-primary" disabled={busy} onClick={save}>{busy ? 'Сохраняем…' : 'Сохранить'}</button>
      </>}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
        <div>
          <div className="label-cap" style={{ marginBottom: 8 }}>Название (метка)</div>
          <input className="inp" value={label} onChange={e => setLabel(e.target.value)} placeholder="Как показывать канал" />
        </div>
        <div>
          <div className="label-cap" style={{ marginBottom: 8 }}>Тип входа по умолчанию</div>
          <div className="seg" style={{ width: '100%' }}>
            {[['signal', 'Как в сигнале'], ['limit', 'Лимит'], ['market', 'Маркет']].map(([k, l]) => (
              <button key={k} className={entryType === k ? 'on' : ''} style={{ flex: 1 }} onClick={() => setEntryType(k)}>{l}</button>
            ))}
          </div>
          <div className="muted" style={{ fontSize: 11, marginTop: 6 }}>
            «Лимит» — отложенный ордер на entry из сигнала. «Маркет» — сразу по рынку. «Как в сигнале» — как пишет канал.
          </div>
        </div>
        <div>
          <div className="label-cap" style={{ marginBottom: 8 }}>Стоп-лосс % (если канал не указал)</div>
          <NumField value={slPct} onChange={setSlPct} suffix="%" hint="0 = не подставлять SL" />
        </div>

        {/* Бэктест канала */}
        <div className="divider" />
        <div className="row between">
          <div>
            <div className="label-cap">Бэктест канала</div>
            <div className="muted" style={{ fontSize: 11, marginTop: 2 }}>Прогон последних 50 сигналов по истории цены</div>
          </div>
          <button className="btn btn-ghost btn-sm" disabled={btBusy} onClick={runBacktest}>
            <Icon name="stats" size={14} />{btBusy ? 'Считаем…' : 'Прогнать'}
          </button>
        </div>
        {btBusy && <>
          <div className="muted" style={{ fontSize: 11.5, marginBottom: 8 }}>
            Считается в фоне (парсим историю канала) — можно закрыть окно, результат придёт уведомлением в бота и на сайт.
          </div>
          <div className="skel" style={{ height: 90, borderRadius: 10 }} />
        </>}
        {bt && (() => {
          const REASONS = { not_listed: 'нет на бирже', empty_klines: 'нет свечей', no_exchange: 'биржа не подключена', exchange_unavailable: 'биржа недоступна', unknown: 'нет данных' };
          const fd = (ms) => ms ? new Date(ms).toLocaleDateString('ru-RU') : '';
          const period = (bt.period_from && bt.period_to)
            ? (fd(bt.period_from) === fd(bt.period_to) ? fd(bt.period_from) : `${fd(bt.period_from)} — ${fd(bt.period_to)}`)
            : '';
          const samples = bt.samples || [];
          const trades = samples.filter(s => s.outcome === 'tp' || s.outcome === 'sl' || s.outcome === 'partial');
          const noData = samples.filter(s => s.outcome === 'no_data');
          const hasVerdict = bt.signals > 0 && bt.evaluated > 0;
          const byReason = {};
          noData.forEach(s => { const r = s.reason || 'unknown'; (byReason[r] = byReason[r] || []).push(s.symbol); });

          const NoDataBlock = noData.length > 0 ? (
            <div style={{ marginTop: 10, fontSize: 11 }}>
              <div className="label-cap" style={{ fontSize: 9.5, marginBottom: 4 }}>Без данных ({noData.length})</div>
              {Object.entries(byReason).map(([r, syms]) => (
                <div key={r} className="muted" style={{ marginBottom: 2 }}>
                  {REASONS[r] || r}: <span className="mono">{[...new Set(syms)].join(', ')}</span>
                </div>
              ))}
            </div>
          ) : null;

          return (
            <div className="card" style={{
              padding: '14px 16px', background: 'var(--bg-2)',
              border: `1px solid ${bt.verdict === 'copy' ? 'var(--green)' : bt.verdict === 'skip' ? 'var(--red)' : 'var(--border)'}`,
            }}>
              {period && <div className="muted" style={{ fontSize: 11, marginBottom: 8 }}>Период: {period} · просмотрено {bt.scanned || 0} сообщений</div>}
              {hasVerdict ? <>
                <div className="row between" style={{ marginBottom: 10 }}>
                  <span style={{ fontWeight: 700, fontSize: 14, color: bt.verdict === 'copy' ? 'var(--green)' : bt.verdict === 'skip' ? 'var(--red)' : 'var(--amber)' }}>
                    {bt.verdict === 'copy' ? '✓ Копировать' : bt.verdict === 'skip' ? '✕ Рискованно' : '~ Нейтрально'}
                  </span>
                  <span className="muted" style={{ fontSize: 12 }}>{bt.evaluated} сделок оценено</span>
                </div>
                <div className="muted" style={{ fontSize: 12, marginBottom: 12 }}>{bt.verdict_text}</div>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 10 }}>
                  <div><div className="label-cap" style={{ fontSize: 9.5 }}>Win Rate</div><div className="mono" style={{ fontSize: 17, fontWeight: 700, color: bt.win_rate >= 50 ? 'var(--green)' : 'var(--red)' }}>{bt.win_rate}%</div></div>
                  <div><div className="label-cap" style={{ fontSize: 9.5 }}>Сум. PnL</div><div className={cx('mono', pn(bt.total_pnl_pct))} style={{ fontSize: 17, fontWeight: 700 }}>{fmt.sign(bt.total_pnl_pct)}{bt.total_pnl_pct}%</div></div>
                  <div><div className="label-cap" style={{ fontSize: 9.5 }}>Ср. сделка</div><div className={cx('mono', pn(bt.avg_pnl_pct))} style={{ fontSize: 17, fontWeight: 700 }}>{fmt.sign(bt.avg_pnl_pct)}{bt.avg_pnl_pct}%</div></div>
                </div>
                <div className="row" style={{ gap: 14, marginTop: 12, fontSize: 11.5 }}>
                  <span className="pos">{bt.wins}W</span><span className="neg">{bt.losses}L</span>
                  <span className="muted">в работе: {bt.open}</span>
                  {bt.no_data > 0 && <span className="muted">нет данных: {bt.no_data}</span>}
                </div>
                {trades.length > 0 && (
                  <div style={{ marginTop: 12 }}>
                    <div className="label-cap" style={{ fontSize: 9.5, marginBottom: 4 }}>Сделки ({trades.length})</div>
                    <div style={{ maxHeight: 220, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
                      {trades.map((s, i) => (
                        <div key={i} className="row between" style={{ fontSize: 11.5, padding: '3px 0', borderBottom: '1px solid var(--border)' }}>
                          <span className="mono">{s.symbol} <span className="muted">{s.side}</span></span>
                          <span className="row" style={{ gap: 8 }}>
                            {s.of > 0 && <span className="muted">{s.targets_hit}/{s.of}🎯</span>}
                            <span className={cx('mono', pn(s.pnl_pct))} style={{ minWidth: 58, textAlign: 'right' }}>{fmt.sign(s.pnl_pct)}{s.pnl_pct}%</span>
                          </span>
                        </div>
                      ))}
                    </div>
                  </div>
                )}
                {NoDataBlock}
              </> : <>
                <div className="muted" style={{ fontSize: 13, color: bt.ok === false ? 'var(--red)' : undefined }}>{bt.error || bt.message || bt.verdict_text || 'Недостаточно данных для бэктеста.'}</div>
                {NoDataBlock}
              </>}
            </div>
          );
        })()}
      </div>
    </Modal>
  );
}

function SignalsChannelsTab() {
  const [tick, setTick] = useState(0);
  const [chansRaw] = useLiveData(() => API().listChannels(), null, [tick]);
  const [editChan, setEditChan] = useState(null);
  const [adding, setAdding] = useState(false);
  const [newName, setNewName] = useState('');
  const toast = useToast();
  const chans = (chansRaw || []).map(mapChannel);
  const reload = () => setTick(t => t + 1);
  const toggleChannel = async (c) => {
    // бэк ждёт is_active, не enabled
    try { await API().updateChannel(c.id, { is_active: !c.on }); toast(c.on ? 'Канал выключен' : 'Канал включён', c.on ? 'info' : 'success'); reload(); }
    catch (e) { toast('Не удалось обновить: ' + (e.message || e), 'error'); }
  };
  const removeChannel = async (c) => {
    if (!confirm(`Удалить канал ${c.name}?`)) return;
    try { await API().removeChannel(c.id); toast('Канал удалён', 'info'); reload(); }
    catch (e) { toast('Не удалось удалить: ' + (e.message || e), 'error'); }
  };
  const addNew = async () => {
    if (!newName) return;
    try {
      // sig: addChannel(channelRef: string, label?: string)
      await API().addChannel(newName.trim(), '');
      toast('Канал добавлен', 'success'); setNewName(''); setAdding(false); reload();
    }
    catch (e) { toast('Не удалось добавить: ' + (e.message || e), 'error'); }
  };
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <div className="row between">
        <SectionHead title="Подключённые каналы" count={chans.length} />
        <button className="btn btn-primary btn-sm" onClick={() => setAdding(true)}><Icon name="plus" size={14} />Добавить канал</button>
      </div>
      <div className="card" style={{ overflow: 'hidden' }}>
        <table className="tbl">
          <thead><tr><th>Канал</th><th className="num">Сигналов · 30д</th><th className="num">Win rate</th><th className="num">PnL · 30д</th><th>Статус</th><th></th></tr></thead>
          <tbody>
            {chans.map(c => (
              <tr key={c.id}>
                <td className="clickable" onClick={() => setEditChan(c)}><div className="row" style={{ gap: 10 }}><span style={{ color: c.accent }}><Icon name="signal" size={16} /></span><div><div className="strong">{c.name}</div><div className="muted mono" style={{ fontSize: 11 }}>{c.tg}</div></div></div></td>
                <td className="num mono">{c.signals30}</td>
                <td className="num mono">{c.winRate}%</td>
                <td className={cx('num mono', pn(c.pnl30))}>{fmt.sign(c.pnl30)}{fmt.compact(c.pnl30)}</td>
                <td><div className={cx('tgl', c.on && 'on')} onClick={() => toggleChannel(c)} /></td>
                <td className="num">
                  <div className="row" style={{ gap: 4, justifyContent: 'flex-end' }}>
                    <button className="btn btn-ghost btn-icon btn-sm" title="Настройки" onClick={() => setEditChan(c)}><Icon name="edit" size={14} /></button>
                    <button className="btn btn-ghost btn-icon btn-sm" title="Удалить" style={{ color: 'var(--red)' }} onClick={() => removeChannel(c)}><Icon name="trash" size={14} /></button>
                  </div>
                </td>
              </tr>
            ))}
            {!chans.length && <tr><td colSpan={6}><Empty icon="signal" title="Каналов нет" text="Добавь Telegram-канал с сигналами." /></td></tr>}
          </tbody>
        </table>
      </div>
      {editChan && <ChannelSettingsModal c={editChan} onClose={() => setEditChan(null)} onSaved={reload} onDeleted={reload} />}
      {adding && (
        <Modal title="Добавить канал" onClose={() => setAdding(false)} width={460}
          footer={<>
            <button className="btn btn-ghost" onClick={() => setAdding(false)}>Отмена</button>
            <button className="btn btn-primary" onClick={addNew}>Добавить</button>
          </>}>
          <div className="label-cap" style={{ marginBottom: 8 }}>Telegram канал</div>
          <input className="inp mono" placeholder="@channel или -100..." value={newName} onChange={e => setNewName(e.target.value)} />
          <div className="muted" style={{ fontSize: 12, marginTop: 12 }}>Вставьте ссылку или ID канала с сигналами.</div>
        </Modal>
      )}
    </div>
  );
}

function SignalsLiveTab() {
  // Берём 30 последних trades — pending/filled/failed/skipped — без фильтра по статусу
  const [tick, setTick] = useState(0);
  const [tradesRaw, loading] = useLiveData(
    async () => { const r = await fetch('/api/trades?limit=30', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, [tick], 30000
  );
  const [botsRaw] = useLiveData(() => API().listLocalBots(), null, []);
  const [chansRaw] = useLiveData(() => API().listChannels(), null, []);
  const [tgStatus] = useLiveData(
    async () => { const r = await fetch('/api/me/telegram/status', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, []
  );
  const toast = useToast();
  const localBots = botsRaw ? botsRaw.map(mapLocalBot) : [];
  const chans = chansRaw ? chansRaw.map(mapChannel) : [];
  const tgConnected = !!(tgStatus?.tg_username || tgStatus?.phone_masked || tgStatus?.connected);
  const activeChannels = chans.filter(c => c.on);
  const botLinked = !!(tgStatus && tgStatus.bot_linked);

  // Onboarding-диагностика: где именно цепочка обрывается
  const diagnostic = !tgConnected
    ? { icon: 'telegram', title: 'Telegram не подключён', text: 'Сигналы из каналов читает listener Telegram. Открой Настройки → Telegram → Подключить.', action: 'settings' }
    : activeChannels.length === 0
    ? { icon: 'signal', title: 'Нет активных каналов', text: 'Telegram подключён, но активных каналов-источников нет. Открой Сигналы → Каналы и включи нужные.', action: 'channels' }
    : !botLinked
    ? { icon: 'bot', title: 'Бот-токен не привязан', text: 'Сигналы читаются, но для исполнения нужен Telegram-бот. Открой Настройки → Telegram и внизу вставь токен бота от @BotFather.', action: 'settings' }
    : null;

  const STATUS_MAP = {
    pending: { c: 'var(--amber)', t: 'Ожидание' },
    filled: { c: 'var(--green)', t: 'Исполнен' },
    closed: { c: 'var(--text-muted)', t: 'Закрыт' },
    cancelled: { c: 'var(--text-muted)', t: 'Отменён' },
    failed: { c: 'var(--red)', t: 'Ошибка' },
    skipped: { c: 'var(--text-muted)', t: 'Пропущен' },
    open: { c: 'var(--green)', t: 'Открыт' },
  };

  const items = (tradesRaw || []).map(t => {
    const parsed = t.signal_parsed || {};
    const dt = new Date(t.opened_at || t.created_at);
    const minsAgo = !isNaN(dt) ? Math.round((Date.now() - dt.getTime()) / 60000) : 0;
    const ago = minsAgo < 1 ? 'только что'
              : minsAgo < 60 ? `${minsAgo} мин назад`
              : minsAgo < 1440 ? `${Math.floor(minsAgo / 60)} ч назад`
              : `${Math.floor(minsAgo / 1440)} д назад`;
    const tps = Array.isArray(t.tp) ? t.tp : (Array.isArray(parsed.tp) ? parsed.tp : []);
    return {
      id: 't_' + t.id, tradeId: t.id,
      symbol: t.symbol, side: String(t.direction || 'long').toLowerCase(),
      lev: parseInt(t.leverage || 1),
      source: t.signal_source || parsed.channel || 'канал',
      entry: parseFloat(parsed.entry || t.entry_price || 0),
      sl: parseFloat(parsed.sl || t.sl || 0),
      tps: tps.map(parseFloat),
      status: t.status || 'unknown',
      error: t.error_message || '',
      ago, raw: t.raw_signal || parsed.raw_signal || '',
    };
  });

  const renderCard = (s) => {
    const st = STATUS_MAP[s.status] || { c: 'var(--text-muted)', t: s.status };
    const isErr = s.status === 'failed' || s.status === 'skipped';
    return (
      <div key={s.id} className="card card-pad fade-in" style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        <div className="row between">
          <div className="row" style={{ gap: 10 }}>
            <span className={cx('badge', s.side === 'long' ? 'badge-long' : 'badge-short')}>{s.side.toUpperCase()}</span>
            <span className="strong mono" style={{ fontSize: 15 }}>{s.symbol}</span>
            <span className="muted mono" style={{ fontSize: 12 }}>×{s.lev}</span>
          </div>
          <span className="pill pill-soft"><span className="dot" style={{ background: st.c }} />{st.t}</span>
        </div>
        <div className="row" style={{ gap: 8 }}><Icon name="signal" size={14} style={{ color: 'var(--accent)' }} /><span className="muted" style={{ fontSize: 12.5 }}>{s.source} · {s.ago}</span></div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 8 }}>
          <div className="card" style={{ padding: '6px 10px', background: 'var(--bg-2)' }}><div className="label-cap" style={{ fontSize: 9 }}>Вход</div><div className="mono" style={{ fontSize: 12, fontWeight: 600 }}>{s.entry || '—'}</div></div>
          <div className="card" style={{ padding: '6px 10px', background: 'var(--bg-2)' }}><div className="label-cap" style={{ fontSize: 9 }}>TP1</div><div className="mono pos" style={{ fontSize: 12, fontWeight: 600 }}>{s.tps[0] || '—'}</div></div>
          <div className="card" style={{ padding: '6px 10px', background: 'var(--bg-2)' }}><div className="label-cap" style={{ fontSize: 9 }}>SL</div><div className="mono neg" style={{ fontSize: 12, fontWeight: 600 }}>{s.sl || '—'}</div></div>
        </div>
        {isErr && s.error && (
          <div className="card" style={{ padding: '8px 10px', background: 'var(--red-soft)', border: '1px solid var(--red)', display: 'flex', gap: 8 }}>
            <Icon name="warning" size={14} style={{ color: 'var(--red)', flex: 'none', marginTop: 2 }} />
            <span style={{ fontSize: 11.5, color: 'var(--text)' }}>{s.error}</span>
          </div>
        )}
      </div>
    );
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      {/* Onboarding-диагностика: где цепочка сломалась */}
      {diagnostic && (
        <div className="card card-pad" style={{ background: 'var(--amber-soft)', border: '1px solid var(--amber)', display: 'flex', gap: 14, alignItems: 'center' }}>
          <div style={{ width: 44, height: 44, borderRadius: 12, background: 'rgba(245,158,11,0.18)', color: 'var(--amber)', display: 'grid', placeItems: 'center', flex: 'none' }}>
            <Icon name={diagnostic.icon} size={22} />
          </div>
          <div style={{ flex: 1 }}>
            <div style={{ fontWeight: 700, fontSize: 14 }}>{diagnostic.title}</div>
            <div className="muted" style={{ fontSize: 12.5, marginTop: 2, lineHeight: 1.45 }}>{diagnostic.text}</div>
          </div>
          <button className="btn btn-primary btn-sm" onClick={() => {
            if (diagnostic.action === 'settings') window.location.hash = '#/settings';
            else if (diagnostic.action === 'channels') {/* остаёмся, переключим под-таб */ const t = document.querySelector('button.on + button:not(.on)'); /* no-op fallback */}
            else if (diagnostic.action === 'bots') window.location.hash = '#/bots';
          }}>Открыть</button>
        </div>
      )}
      {/* Pipeline status: показываем зелёным когда всё нормально */}
      {!diagnostic && items.length === 0 && (
        <div className="card card-pad" style={{ background: 'var(--green-soft)', border: '1px solid var(--green)', display: 'flex', gap: 14, alignItems: 'center' }}>
          <Icon name="checkcircle" size={22} style={{ color: 'var(--green)', flex: 'none' }} />
          <div style={{ flex: 1 }}>
            <div style={{ fontWeight: 700, fontSize: 14 }}>Pipeline активен</div>
            <div className="muted" style={{ fontSize: 12.5, marginTop: 2 }}>Telegram · {activeChannels.length} канал(ов) · {localBots.filter(b => b.status === 'running').length} активн. ботов. Жду новых сигналов из каналов.</div>
          </div>
        </div>
      )}
      <div className="row between">
        <SectionHead title="Лента сигналов" count={items.length} />
        <div className="row" style={{ gap: 8 }}>
          {items.some(s => s.status === 'pending') && (
            <button className="btn btn-ghost btn-sm" style={{ color: 'var(--amber)' }} onClick={async () => {
              const n = items.filter(s => s.status === 'pending').length;
              if (!confirm(`Отменить все ${n} ожидающих сигналов?`)) return;
              try {
                const r = await fetch('/api/trades/cancel-all-pending', { method: 'POST', credentials: 'include' });
                const j = await r.json();
                toast(`Отменено ${j.cancelled || 0} сигналов`, 'success');
                setTick(t => t + 1);
              } catch { toast('Не удалось очистить очередь', 'error'); }
            }}><Icon name="trash" size={14} />Очистить очередь</button>
          )}
          <button className="btn btn-ghost btn-sm" onClick={() => setTick(t => t + 1)}><Icon name="refresh" size={14} />Обновить</button>
        </div>
      </div>
      {items.length ? (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 16 }}>
          {items.map(renderCard)}
        </div>
      ) : (
        <div className="card">
          <Empty icon="coffee" title="Сигналов пока нет" text={loading ? 'Загружаем…' : 'Подключи Telegram, активируй каналы — и сигналы появятся здесь автоматически.'} />
        </div>
      )}
    </div>
  );
}

function ForwardRuleModal({ rule, channels, onClose, onSaved }) {
  // rule=null → создание; иначе редактирование
  const editing = !!rule;
  const [source, setSource] = useState(rule?.source_channel_id != null ? String(rule.source_channel_id) : '');
  const [dest, setDest] = useState(rule?.dest_channel_ref || '');
  const [label, setLabel] = useState(rule?.label || '');
  const [side, setSide] = useState((rule?.filter?.side) || 'all');
  const [symbols, setSymbols] = useState((rule?.filter?.symbols || []).join(', '));
  const [busy, setBusy] = useState(false);
  const toast = useToast();

  const save = async () => {
    if (!dest.trim()) { toast('Укажи целевой канал', 'error'); return; }
    setBusy(true);
    const filter = {
      side,
      symbols: symbols.split(',').map(s => s.trim().toUpperCase()).filter(Boolean),
    };
    try {
      if (editing) {
        await API().updateForward(rule.id, { label, dest_channel_ref: dest.trim(), filter });
      } else {
        await API().addForward({
          source_channel_id: source ? parseInt(source) : null,
          dest_channel_ref: dest.trim(),
          label, filter,
        });
      }
      toast(editing ? 'Правило обновлено' : 'Правило создано', 'success');
      onSaved(); onClose();
    } catch (e) { toast('Не удалось: ' + (e.message || e), 'error'); }
    setBusy(false);
  };

  return (
    <Modal title={editing ? 'Настройка правила' : 'Новое правило'} sub="Сигналы из источника пересылаются в целевой канал" onClose={onClose} width={500}
      footer={<>
        <button className="btn btn-ghost" disabled={busy} onClick={onClose}>Отмена</button>
        <button className="btn btn-primary" disabled={busy} onClick={save}>{busy ? 'Сохраняем…' : 'Сохранить'}</button>
      </>}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
        <div>
          <div className="label-cap" style={{ marginBottom: 8 }}>Источник</div>
          <select className="inp" value={source} onChange={e => setSource(e.target.value)}>
            <option value="">Любой канал (catch-all)</option>
            {channels.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
          </select>
        </div>
        <div>
          <div className="label-cap" style={{ marginBottom: 8 }}>Целевой канал (куда пересылать)</div>
          <input className="inp mono" value={dest} onChange={e => setDest(e.target.value)} placeholder="@my_channel или -100…" />
          <div className="muted" style={{ fontSize: 11, marginTop: 6 }}>Твой Telegram-аккаунт должен быть участником этого канала с правом писать.</div>
        </div>
        <div>
          <div className="label-cap" style={{ marginBottom: 8 }}>Метка (опц.)</div>
          <input className="inp" value={label} onChange={e => setLabel(e.target.value)} placeholder="например: VIP → личный архив" />
        </div>
        <div className="divider" />
        <div className="label-cap">Точечный фильтр (что пересылать)</div>
        <div>
          <div className="muted" style={{ fontSize: 11.5, marginBottom: 6 }}>Направление</div>
          <div className="seg" style={{ width: '100%' }}>
            {[['all', 'Все'], ['long', 'Только LONG'], ['short', 'Только SHORT']].map(([k, l]) => (
              <button key={k} style={{ flex: 1 }} className={side === k ? 'on' : ''} onClick={() => setSide(k)}>{l}</button>
            ))}
          </div>
        </div>
        <div>
          <div className="muted" style={{ fontSize: 11.5, marginBottom: 6 }}>Только эти монеты (через запятую, пусто = все)</div>
          <input className="inp mono" value={symbols} onChange={e => setSymbols(e.target.value.toUpperCase())} placeholder="BTCUSDT, ETHUSDT" />
        </div>
      </div>
    </Modal>
  );
}

function SignalsForwardsTab() {
  const [tick, setTick] = useState(0);
  const [raw] = useLiveData(() => API().listForwards(), null, [tick]);
  const [chansRaw] = useLiveData(() => API().listChannels(), null, [tick]);
  const [editRule, setEditRule] = useState(undefined); // undefined=закрыто, null=создание, obj=правка
  const toast = useToast();
  const channels = chansRaw ? chansRaw.map(mapChannel) : [];
  const rules = raw || [];
  const reload = () => setTick(t => t + 1);

  const fmtFilter = (f) => {
    if (!f || (!f.side && !(f.symbols || []).length)) return 'все сигналы';
    const parts = [];
    if (f.side === 'long') parts.push('LONG');
    else if (f.side === 'short') parts.push('SHORT');
    if ((f.symbols || []).length) parts.push(f.symbols.slice(0, 3).join('/') + (f.symbols.length > 3 ? '…' : ''));
    return parts.join(' · ') || 'все сигналы';
  };
  const toggleRule = async (r) => {
    try { await API().updateForward(r.id, { enabled: !r.enabled }); reload(); }
    catch (e) { toast('Не удалось: ' + (e.message || e), 'error'); }
  };
  const deleteRule = async (r) => {
    if (!confirm('Удалить это правило перенаправления?')) return;
    try { await API().deleteForward(r.id); reload(); toast('Правило удалено', 'info'); }
    catch (e) { toast('Не удалось удалить: ' + (e.message || e), 'error'); }
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <div className="row between">
        <SectionHead title="Перенаправления сигналов" count={rules.length} />
        <button className="btn btn-primary btn-sm" onClick={() => setEditRule(null)}><Icon name="plus" size={14} />Новое правило</button>
      </div>
      {rules.length ? (
        <div className="card" style={{ overflow: 'hidden' }}>
          <table className="tbl">
            <thead><tr><th>Источник</th><th></th><th>Целевой канал</th><th>Фильтр</th><th>Статус</th><th></th></tr></thead>
            <tbody>{rules.map(r => (
              <tr key={r.id} className="clickable" onClick={() => setEditRule(r)}>
                <td><span className="pill pill-soft"><span className="dot" style={{ background: 'var(--accent)' }} />{r.source_name || 'любой канал'}</span></td>
                <td><Icon name="forward" size={16} style={{ color: 'var(--text-muted)' }} /></td>
                <td className="mono">{r.label ? <span>{r.label} <span className="muted" style={{ fontSize: 11 }}>{r.dest_channel_ref}</span></span> : r.dest_channel_ref}</td>
                <td className="muted" style={{ fontSize: 12 }}>{fmtFilter(r.filter)}</td>
                <td><div className={cx('tgl', r.enabled && 'on')} onClick={e => { e.stopPropagation(); toggleRule(r); }} /></td>
                <td className="num">
                  <div className="row" style={{ gap: 4, justifyContent: 'flex-end' }}>
                    <button className="btn btn-ghost btn-icon btn-sm" title="Настроить" onClick={e => { e.stopPropagation(); setEditRule(r); }}><Icon name="edit" size={14} /></button>
                    <button className="btn btn-ghost btn-icon btn-sm" title="Удалить" style={{ color: 'var(--red)' }} onClick={e => { e.stopPropagation(); deleteRule(r); }}><Icon name="trash" size={14} /></button>
                  </div>
                </td>
              </tr>
            ))}</tbody>
          </table>
        </div>
      ) : <Empty icon="forward" title="Правил пока нет" text="Добавь правило: сигналы из канала A → твой личный канал B (с фильтром по направлению/монетам)." action={<button className="btn btn-primary" onClick={() => setEditRule(null)}>Новое правило</button>} />}
      {editRule !== undefined && <ForwardRuleModal rule={editRule} channels={channels} onClose={() => setEditRule(undefined)} onSaved={reload} />}
    </div>
  );
}

function Signals() {
  const [sub, setSub] = useState('channels');
  return (
    <div className="fade-in" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 18 }}>
      <div className="tabs" style={{ border: 'none' }}>
        {[['channels', 'Каналы'], ['live', 'Live лента'], ['forwards', 'Перенаправления']].map(([k, l]) => (
          <button key={k} className={sub === k ? 'on' : ''} onClick={() => setSub(k)}>{l}</button>
        ))}
      </div>
      {sub === 'channels' && <SignalsChannelsTab />}
      {sub === 'live' && <SignalsLiveTab />}
      {sub === 'forwards' && <SignalsForwardsTab />}
    </div>
  );
}

/* ===================== JOURNAL (with source filter) ===================== */
function Journal() {
  const [side, setSide] = useState('all');
  const [result, setResult] = useState('all');
  const [q, setQ] = useState('');
  // Активная биржа юзера
  const [meSettings] = useLiveData(
    async () => { const r = await fetch('/api/me/settings', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, []
  );
  const activeEx = (meSettings?.active_exchange || 'weex').toLowerCase();
  // Реальная история сделок С БИРЖИ по ключам юзера (изолировано, не глобальная /api/trades)
  const [tradesRaw, loading] = useLiveData(
    async () => {
      const r = await fetch(`/api/ex/${activeEx}/closed-trades`, { credentials: 'include' });
      if (!r.ok) return { trades: [] };
      return r.json();
    },
    { trades: [] }, [activeEx], 60000
  );
  const allTrades = useMemo(() => {
    const list = (tradesRaw?.trades || []).map((t, i) => {
      const sideL = String(t.side || 'LONG').toLowerCase();
      const entry = parseFloat(t.entry_price || 0);
      const exit = parseFloat(t.exit_price || 0);
      const pnl = parseFloat(t.pnl || 0);
      const pnlPct = entry > 0 ? ((sideL === 'long' ? (exit - entry) : (entry - exit)) / entry * 100) : 0;
      const dur = t.duration_sec ? (t.duration_sec >= 3600 ? Math.round(t.duration_sec / 3600) + 'ч' : Math.round(t.duration_sec / 60) + 'м') : '—';
      return {
        id: (t.symbol || '') + '_' + (t.closed_at || i),
        symbol: t.symbol, side: sideL,
        qty: parseFloat(t.qty || 0), entry, exit, pnl, pnlPct,
        fee: parseFloat(t.fee || 0),
        win: pnl >= 0, closed: t.closed_at, dur,
      };
    });
    return list;
  }, [tradesRaw]);
  const rows = useMemo(() => allTrades.filter(t =>
    (side === 'all' || t.side === side) &&
    (result === 'all' || (result === 'win' ? t.win : !t.win)) &&
    (!q || (t.symbol || '').toLowerCase().includes(q.toLowerCase()))
  ), [allTrades, side, result, q]);
  const totalPnl = rows.reduce((s, t) => s + t.pnl, 0);
  const wins = rows.filter(t => t.win).length;

  const exportCsv = () => {
    const head = 'Пара,Сторона,Кол-во,Вход,Выход,PnL,PnL%,Комиссия,Закрыта\n';
    const body = rows.map(t => `${t.symbol},${t.side},${t.qty},${t.entry},${t.exit},${t.pnl},${t.pnlPct.toFixed(2)},${t.fee},${t.closed}`).join('\n');
    const blob = new Blob([head + body], { type: 'text/csv' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = `journal_${activeEx}.csv`;
    a.click();
  };

  return (
    <div className="fade-in" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 16 }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16 }}>
        <Kpi label="Сделок" value={rows.length} sub={activeEx.toUpperCase()} icon="journal" />
        <Kpi label="Совокупный P&L" value={<span className={pn(totalPnl)}>{fmt.sign(totalPnl)}{fmt.usdt(totalPnl)}</span>} icon="money" />
        <Kpi label="Win rate" value={Math.round(wins / (rows.length || 1) * 100) + '%'} sub={`${wins}W / ${rows.length - wins}L`} icon="target" />
        <Kpi label="Ср. сделка" value={<span className={pn(totalPnl)}>{fmt.sign(totalPnl / (rows.length || 1))}{fmt.usdt(totalPnl / (rows.length || 1))}</span>} icon="scale" />
      </div>
      <div className="card">
        <div className="card-pad" style={{ paddingBottom: 14 }}>
          <div className="row between wrap" style={{ gap: 12 }}>
            <div className="row wrap" style={{ gap: 10 }}>
              <div className="seg">{[['all', 'Все'], ['long', 'Long'], ['short', 'Short']].map(([k, l]) => <button key={k} className={side === k ? 'on' : ''} onClick={() => setSide(k)}>{l}</button>)}</div>
              <div className="seg">{[['all', 'Все'], ['win', 'Прибыль'], ['loss', 'Убыток']].map(([k, l]) => <button key={k} className={result === k ? 'on' : ''} onClick={() => setResult(k)}>{l}</button>)}</div>
            </div>
            <div className="row" style={{ gap: 8 }}>
              <div style={{ position: 'relative' }}>
                <Icon name="search" size={15} style={{ position: 'absolute', left: 10, top: 11, color: 'var(--text-muted)' }} />
                <input className="inp" style={{ width: 180, paddingLeft: 32 }} placeholder="Пара…" value={q} onChange={e => setQ(e.target.value)} />
              </div>
              <button className="btn btn-ghost btn-sm" onClick={exportCsv}><Icon name="download" size={15} />Экспорт</button>
            </div>
          </div>
        </div>
        {rows.length ? (
          <div style={{ maxHeight: 'calc(100vh - 400px)', overflowY: 'auto' }}>
            <table className="tbl">
              <thead><tr><th>Пара</th><th>Сторона</th><th className="num">Кол-во</th><th className="num">Вход</th><th className="num">Выход</th><th className="num">PnL</th><th className="num">%</th><th className="num">Длит.</th><th className="num">Закрыта</th></tr></thead>
              <tbody>{rows.map(t => (
                <tr key={t.id}>
                  <td className="strong mono">{t.symbol}</td>
                  <td><span className={cx('badge', t.side === 'long' ? 'badge-long' : 'badge-short')}>{t.side.toUpperCase()}</span></td>
                  <td className="num mono">{t.qty || '—'}</td>
                  <td className="num mono">{t.entry || '—'}</td>
                  <td className="num mono">{t.exit || '—'}</td>
                  <td className={cx('num mono', pn(t.pnl))} style={{ fontWeight: 600 }}>{fmt.sign(t.pnl)}{fmt.usdt(t.pnl)}</td>
                  <td className={cx('num mono', pn(t.pnlPct))}>{fmt.pct(t.pnlPct)}</td>
                  <td className="num muted mono" style={{ fontSize: 12 }}>{t.dur}</td>
                  <td className="num muted mono" style={{ fontSize: 11.5 }}>{t.closed ? new Date(t.closed).toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' }) : '—'}</td>
                </tr>
              ))}</tbody>
            </table>
          </div>
        ) : (
          <Empty icon="journal" title={loading ? 'Загружаем историю…' : 'Сделок пока нет'}
            text={loading ? '' : `На ${activeEx.toUpperCase()} за последние 30 дней закрытых сделок не найдено. Здесь появится твоя реальная история с биржи.`} />
        )}
      </div>
    </div>
  );
}

/* ===================== EXCHANGE (trading terminal) ====================== */
function Exchange() {
  const [tab, setTab] = useState('positions');
  const [selPos, setSelPos] = useState(null);
  const [bal] = useLiveData(() => API().loadBalance(), null, [], 30000);
  const [posRaw, , refreshPos] = useLiveData(() => API().loadPositions(), null, [], 15000);
  const [closedIds, setClosedIds] = useState(() => new Set());
  const [meSettings] = useLiveData(
    async () => { const r = await fetch('/api/me/settings', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, []
  );
  const activeEx = (meSettings?.active_exchange || 'weex').toLowerCase();
  // Реальные открытые ордера с активной биржи
  const [ordersRaw, ordersLoading] = useLiveData(
    async () => {
      const r = await fetch(`/api/ex/${activeEx}/open-orders`, { credentials: 'include' });
      if (!r.ok) return [];
      const j = await r.json();
      return Array.isArray(j) ? j : (j.orders || j.data || []);
    },
    null, [activeEx], 30000
  );
  const orders = (ordersRaw || []).map((o, i) => ({
    id: o.orderId || o.order_id || o.ordId || ('o' + i),
    symbol: (o.symbol || o.instId || '').replace(/[-_]/g, '').replace('SWAP', ''),
    side: /buy|long/i.test(String(o.side || o.posSide || '')) ? 'long' : 'short',
    type: String(o.orderType || o.ordType || o.type || 'LIMIT').toUpperCase(),
    price: parseFloat(o.price || o.px || 0),
    qty: parseFloat(o.size || o.qty || o.sz || 0),
    status: String(o.status || o.state || 'NEW'),
  }));
  const ex = bal ? {
    equity: bal.equity || 0, available: bal.available || 0, pnlDay: bal.dayPnl || 0,
  } : { equity: 0, available: 0, pnlDay: 0 };
  const positions = (posRaw ? posRaw.map(mapPosition) : []).filter(p => !closedIds.has(p.id));
  useEffect(() => {
    if (!posRaw) return;
    setClosedIds(prev => {
      if (!prev.size) return prev;
      const present = new Set(posRaw.map(mapPosition).map(p => p.id));
      const next = new Set([...prev].filter(id => present.has(id)));
      return next.size === prev.size ? prev : next;
    });
  }, [posRaw]);
  const handlePosClosed = (pct) => {
    if (pct === 100 && selPos) { const id = selPos.id; setClosedIds(s => new Set(s).add(id)); }
    refreshPos();
    setTimeout(refreshPos, 1500);
    setTimeout(refreshPos, 4000);
  };
  const usedMargin = positions.reduce((s, p) => s + (p.margin || 0), 0);
  const unrealized = positions.reduce((s, p) => s + (p.pnl || 0), 0);
  const toast = useToast();
  return (
    <div className="fade-in" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 16 }}>
      {/* Balance summary strip */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16 }}>
        <Kpi label="Эквити" value={fmt.usdt(ex.equity)} sub="USDT" icon="wallet" accent="var(--accent)" />
        <Kpi label="Доступно" value={fmt.usdt(ex.available)} sub="свободная маржа" icon="money" />
        <Kpi label="Использовано" value={fmt.usdt(usedMargin)} sub="в позициях" icon="lock" />
        <Kpi label="Нереализ. PnL" value={<span className={pn(unrealized)}>{fmt.sign(unrealized)}{fmt.usdt(unrealized)}</span>} sub="по открытым позициям" icon="trendup" accent={unrealized > 0 ? 'var(--green)' : 'var(--red)'} />
      </div>

      <div className="card">
        <div className="card-pad" style={{ paddingBottom: 0 }}>
          <div className="row between">
            <SectionHead title={`Терминал · ${activeEx.toUpperCase()}`} />
            <div className="tabs" style={{ border: 'none' }}>
              {[['positions', `Позиции (${positions.length})`], ['orders', `Ордера (${orders.length})`]].map(([k, l]) => (
                <button key={k} className={tab === k ? 'on' : ''} onClick={() => setTab(k)}>{l}</button>
              ))}
            </div>
            <button className="btn btn-danger btn-sm" onClick={async () => {
              if (!confirm('Закрыть ВСЕ позиции по рынку + отменить pending ордера?')) return;
              try {
                // sig: activateStorm(hours, closePositions=true, cancelPending=true). hours=0 → ad-hoc one-shot.
                await API().activateStorm(0, true, true);
                toast('Storm Close запущен', 'success');
              } catch (e) { toast('Storm не сработал: ' + (e.message || e), 'error'); }
            }}><Icon name="flame" size={14} />Storm Close</button>
          </div>
        </div>
        {tab === 'positions' && (
          positions.length ? (
            <table className="tbl">
              <thead><tr><th>Позиция</th><th className="num">Размер</th><th className="num">Вход</th><th className="num">Mark</th><th className="num">Ликвид.</th><th className="num">PnL</th><th>Источник</th></tr></thead>
              <tbody>{positions.map(p => <PositionRow key={p.id} p={p} onClick={() => setSelPos(p)} />)}</tbody>
            </table>
          ) : <Empty icon="layers" title="Нет открытых позиций" text="Откройте сделку вручную или дождитесь сигнала из канала." />
        )}
        {tab === 'orders' && (
          orders.length ? (
            <table className="tbl">
              <thead><tr><th>Символ</th><th>Сторона</th><th>Тип</th><th className="num">Цена</th><th className="num">Кол-во</th><th>Статус</th></tr></thead>
              <tbody>{orders.map(o => (
                <tr key={o.id}>
                  <td className="strong mono">{o.symbol}</td>
                  <td><span className={cx('badge', o.side === 'long' ? 'badge-long' : 'badge-short')}>{o.side.toUpperCase()}</span></td>
                  <td><span className="pill pill-soft">{o.type}</span></td>
                  <td className="num mono">{o.price || '—'}</td>
                  <td className="num mono">{o.qty || '—'}</td>
                  <td><StatusPill status="pending" /></td>
                </tr>
              ))}</tbody>
            </table>
          ) : (
            <Empty icon="journal" title="Нет активных ордеров"
              text={ordersLoading ? 'Загружаем с биржи…' : `На ${activeEx.toUpperCase()} сейчас нет открытых лимитных/стоп-ордеров.`} />
          )
        )}
      </div>
      {selPos && <PositionModal p={selPos} onClose={() => setSelPos(null)} onClosed={handlePosClosed} />}
    </div>
  );
}

/* ===================== SPOT ============================================= */
function Spot() {
  // Узнаём активную биржу из settings — WEEX не поддерживает spot, для неё показываем заглушку
  const [settings] = useLiveData(() => API().loadSettings(), null, [], 60000);
  const activeEx = (settings?.exchange || settings?.active_exchange || 'weex').toLowerCase();
  const spotSupported = activeEx !== 'weex';
  const [spotRaw, loading] = useLiveData(
    () => spotSupported ? API().loadSpotBalance() : Promise.resolve([]),
    null, [activeEx], 60000
  );
  // Парсим что бы ни вернул бэк: массив, {balances}, {data}
  const rawList = Array.isArray(spotRaw) ? spotRaw
    : Array.isArray(spotRaw?.balances) ? spotRaw.balances
    : Array.isArray(spotRaw?.data) ? spotRaw.data
    : [];
  const list = rawList.map(mapSpotHolding).filter(h => h.value > 0.5);
  const total = list.reduce((s, h) => s + (h.value || 0), 0);

  if (!spotSupported) {
    return (
      <div className="fade-in" style={{ padding: 24 }}>
        <div className="card" style={{ padding: 36 }}>
          <Empty
            icon="info"
            title={`Спот не поддерживается на ${activeEx.toUpperCase()}`}
            text="Переключи активную биржу на OKX, Binance или Bybit чтобы видеть свои спот-балансы. WEEX-провайдер API4ATKA работает только с фьючерсами."
          />
        </div>
      </div>
    );
  }
  if (!list.length && !loading) {
    return (
      <div className="fade-in" style={{ padding: 24 }}>
        <div className="card" style={{ padding: 36 }}>
          <Empty icon="spot" title="Нет спот-активов" text={`На ${activeEx.toUpperCase()} сейчас нет монет с балансом > 0.5 USDT.`} />
        </div>
      </div>
    );
  }
  const palette = ['#3B82F6', '#F59E0B', '#14B8A6', '#A855F7', '#22C55E', '#EF4444', '#A855F7', '#7E8081'];
  return (
    <div className="fade-in" style={{ padding: 24 }}>
      <div style={{ display: 'grid', gridTemplateColumns: '320px 1fr', gap: 16 }}>
        <div className="card card-pad" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 }}>
          <div className="label-cap" style={{ alignSelf: 'flex-start' }}>Стоимость портфеля</div>
          <Donut size={150} thickness={18} segments={list.map((h, i) => ({ value: h.value, color: palette[i % palette.length] }))}
            center={<><span className="mono" style={{ fontSize: 20, fontWeight: 700 }}>{fmt.compact(total)}</span><span className="muted" style={{ fontSize: 11 }}>USDT</span></>} />
          <div style={{ width: '100%' }}>
            {list.map((h, i) => (
              <div key={h.sym} className="row between" style={{ fontSize: 12.5, padding: '5px 0' }}>
                <div className="row" style={{ gap: 8 }}><span className="dot" style={{ background: palette[i % palette.length], width: 9, height: 9 }} /><span className="mono strong">{h.sym}</span></div>
                <span className="mono muted">{(h.value / total * 100).toFixed(1)}%</span>
              </div>
            ))}
          </div>
        </div>
        <div className="card" style={{ overflow: 'hidden' }}>
          <div className="card-pad" style={{ paddingBottom: 12 }}><SectionHead title="Спот-активы" count={list.length} /></div>
          <table className="tbl">
            <thead><tr><th>Актив</th><th className="num">Кол-во</th><th className="num">Цена</th><th className="num">Стоимость</th><th className="num">24ч</th><th className="num">Доля</th></tr></thead>
            <tbody>{list.map(h => (
              <tr key={h.sym}>
                <td><div className="row" style={{ gap: 10 }}><Avatar name={h.sym} hue={h.sym === 'BTC' ? 38 : h.sym === 'TON' ? 205 : h.sym === 'SOL' ? 275 : 168} size={28} /><div><div className="strong mono">{h.sym}</div><div className="muted" style={{ fontSize: 11 }}>{h.name}</div></div></div></td>
                <td className="num mono">{fmt.num(h.amount, h.amount < 1 ? 4 : 2)}</td>
                <td className="num mono">{fmt.usd(h.price, h.price < 10 ? 3 : 0)}</td>
                <td className="num mono strong">{fmt.usdt(h.value)}</td>
                <td className={cx('num mono', pn(h.change))}>{fmt.pct(h.change)}</td>
                <td className="num mono muted">{(h.value / total * 100).toFixed(1)}%</td>
              </tr>
            ))}</tbody>
          </table>
        </div>
      </div>
    </div>
  );
}

/* ===================== STATS ============================================ */
function Stats() {
  const [eqRaw] = useLiveData(() => API().loadEquityHistory(60), null);
  const [chanStatsRaw] = useLiveData(() => API().loadChannelStats(30), null);
  // Полная аналитика активной биржи за 30д: win_rate, best/worst, daily[{date,pnl,trades}]
  const [anRaw] = useLiveData(
    async () => {
      const s = await fetch('/api/me/settings', { credentials: 'include' }).then(r => r.ok ? r.json() : null).catch(() => null);
      const ex = (s?.active_exchange || 'weex').toLowerCase();
      const r = await fetch(`/api/ex/${ex}/analytics`, { credentials: 'include' });
      return r.ok ? r.json() : null;
    },
    null, []
  );
  const equity = useMemo(() => {
    if (eqRaw && eqRaw.length) return eqRaw.map(p => ({ t: new Date(p.date || p.t || Date.now()).getTime(), v: p.equity || p.value || p.v || 0 }));
    return [];
  }, [eqRaw]);
  const channelsByPnl = chanStatsRaw && Array.isArray(chanStatsRaw) && chanStatsRaw.length
    ? chanStatsRaw.map(c => ({
        id: c.channel_id || c.id, name: c.title || c.channel_name || 'Канал',
        signals30: c.signals || c.total || 0, pnl30: c.pnl_usdt || 0,
        accent: c.color || '#3B82F6',
      })).filter(c => c.signals30 > 0)
    : [];
  // Дневной P&L: приоритет — analytics.daily (есть и pnl, и кол-во сделок для тултипа),
  // фолбэк — дифф equity-кривой (без сделок).
  const byDay = useMemo(() => {
    if (anRaw?.daily?.length) {
      return anRaw.daily.slice(-30).map(d => ({
        d: d.date || d.day || '',
        v: +(parseFloat(d.pnl ?? d.net_pnl ?? 0)).toFixed(2),
        trades: d.trades ?? d.count ?? null,
      }));
    }
    if (eqRaw && eqRaw.length > 1) {
      return eqRaw.slice(-30).map((p, i, a) => ({ d: p.date || '', v: i > 0 ? +(p.equity - a[i - 1].equity).toFixed(2) : 0, trades: null }));
    }
    return [];
  }, [anRaw, eqRaw]);
  const maxBar = Math.max(...byDay.map(b => Math.abs(b.v)), 1);
  const [hoverBar, setHoverBar] = useState(null);

  // KPI из реальной аналитики (если бэк отдал)
  const netPnl = anRaw?.net_pnl;
  const winRate = anRaw?.win_rate;
  const best = anRaw?.best;
  const rm = anRaw?.risk_metrics || {};
  // Просадка: % — относительно эквити (бэк), при отсутствии — абсолют в USDT.
  const maxDdPct = rm.max_drawdown_pct ?? rm.max_dd_pct ?? null;
  const maxDdUsd = rm.max_drawdown ?? null;
  const profitFactor = rm.profit_factor ?? null;
  return (
    <div className="fade-in" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 16 }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16 }}>
        <Kpi label="Net PnL · 30д" value={netPnl != null ? <span className={pn(netPnl)}>{fmt.sign(netPnl)}{fmt.usdt(netPnl)}</span> : '—'} sub={winRate != null ? `win rate ${winRate}%` : 'USDT за вычетом комиссий'} icon="trendup" accent="var(--green)" />
        <Kpi label="Profit factor" value={profitFactor != null ? Number(profitFactor).toFixed(2) : '—'} sub="прибыль / убыток" icon="scale" />
        <Kpi label="Лучшая сделка" value={best ? <span className="pos">+{fmt.usdt(best.pnl)}</span> : '—'} sub={best ? `${best.symbol} · ${best.side}` : 'за 30 дней'} icon="trophy" accent="var(--amber)" />
        <Kpi label="Макс. просадка" value={maxDdPct != null ? <span className="neg">-{Math.abs(maxDdPct).toFixed(1)}%</span> : (maxDdUsd ? <span className="neg">-{fmt.usdt(Math.abs(maxDdUsd))}</span> : '—')} sub={maxDdPct != null && maxDdUsd ? `-${fmt.usdt(Math.abs(maxDdUsd))} USDT` : 'за период'} icon="trenddown" accent="var(--red)" />
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 360px', gap: 16 }}>
        <div className="card card-pad">
          <div className="label-cap" style={{ marginBottom: 8 }}>Кривая капитала · 60 дней</div>
          {equity.length >= 2
            ? <AreaChart data={equity} height={240} color="var(--accent)" />
            : <div style={{ height: 240 }}><Empty icon="stats" title="Копим историю" text="Кривая капитала строится по ежечасным снимкам баланса — появится в течение суток." /></div>}
        </div>
        <div className="card card-pad">
          <div className="label-cap" style={{ marginBottom: 16 }}>Каналы · сигналы и P&L · 30д</div>
          {!channelsByPnl.length && <Empty icon="stats" title="Нет сигналов" text="Появится после первых сделок по каналам." />}
          {channelsByPnl.map(c => {
            const max = Math.max(...channelsByPnl.map(x => x.signals30), 1);
            return (
              <div key={c.id} style={{ marginBottom: 14 }}>
                <div className="row between" style={{ fontSize: 12.5, marginBottom: 5 }}><span>{c.name}</span><span className={cx('mono', pn(c.pnl30))}>{fmt.sign(c.pnl30)}{fmt.compact(c.pnl30)}</span></div>
                <div style={{ height: 7, borderRadius: 99, background: 'var(--bg-3)', overflow: 'hidden' }}><div style={{ width: `${c.signals30 / max * 100}%`, height: '100%', background: c.accent }} /></div>
              </div>
            );
          })}
        </div>
      </div>
      <div className="card card-pad">
        <div className="label-cap" style={{ marginBottom: 16 }}>Дневной P&L · 30 дней</div>
        {byDay.length ? (
          <div style={{ position: 'relative' }} onMouseLeave={() => setHoverBar(null)}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 3, height: 140 }}>
              {byDay.map((b, i) => {
                const active = hoverBar === i;
                return (
                  <div
                    key={i}
                    onMouseEnter={() => setHoverBar(i)}
                    style={{ flex: 1, height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center', cursor: 'pointer', opacity: hoverBar == null || active ? 1 : 0.45, transition: 'opacity .12s' }}
                  >
                    <div style={{ display: 'flex', flexDirection: 'column', justifyContent: b.v >= 0 ? 'flex-end' : 'flex-start', height: '100%' }}>
                      {b.v >= 0
                        ? <><div style={{ flex: 1 }} /><div style={{ height: `${Math.abs(b.v) / maxBar * 48}%`, background: 'var(--green)', borderRadius: '3px 3px 0 0', minHeight: 2, boxShadow: active ? '0 0 0 1px var(--green)' : 'none' }} /><div style={{ height: '50%' }} /></>
                        : <><div style={{ height: '50%' }} /><div style={{ height: `${Math.abs(b.v) / maxBar * 48}%`, background: 'var(--red)', borderRadius: '0 0 3px 3px', minHeight: 2, boxShadow: active ? '0 0 0 1px var(--red)' : 'none' }} /><div style={{ flex: 1 }} /></>}
                    </div>
                  </div>
                );
              })}
            </div>
            {hoverBar != null && byDay[hoverBar] && (() => {
              const b = byDay[hoverBar];
              const left = (hoverBar + 0.5) / byDay.length * 100;
              return (
                <div style={{
                  position: 'absolute', top: -8, left: `${left}%`, transform: 'translate(-50%,-100%)',
                  background: 'var(--bg-1, #0e1117)', border: '1px solid var(--border, #2a2f3a)', borderRadius: 8,
                  padding: '7px 10px', fontSize: 12, lineHeight: 1.5, whiteSpace: 'nowrap', pointerEvents: 'none',
                  boxShadow: '0 8px 24px rgba(0,0,0,.45)', zIndex: 5,
                }}>
                  {b.d && <div style={{ color: 'var(--text-3, #8b93a7)', marginBottom: 2 }}>{b.d}</div>}
                  <div className={cx('mono', pn(b.v))} style={{ fontWeight: 600 }}>{fmt.sign(b.v)}{fmt.usdt(b.v)} USDT</div>
                  {b.trades != null && <div style={{ color: 'var(--text-3, #8b93a7)' }}>сделок: {b.trades}</div>}
                </div>
              );
            })()}
          </div>
        ) : <Empty icon="stats" title="Пока нет данных" text="Дневной P&L появится после первых закрытых сделок." />}
      </div>
    </div>
  );
}

/* ===================== SETTINGS (with API & Push) ======================= */
function ConnectApiModal({ ex, onClose, onSave }) {
  const [key, setKey] = useState('');
  const [secret, setSecret] = useState('');
  const [pass, setPass] = useState('');
  const [busy, setBusy] = useState(false);
  // PASSPHRASE_REQUIRED = ['weex', 'okx'] — берём из MobileAPI чтобы не дублировать список
  const passRequired = (API()?.PASSPHRASE_REQUIRED || ['weex', 'okx']).includes(ex.id?.toLowerCase() || ex.name?.toLowerCase());
  const toast = useToast();
  const submit = async () => {
    const k = key.trim(), s = secret.trim(), p = pass.trim();
    if (!k || !s) { toast('Заполни API Key и Secret', 'error'); return; }
    if (passRequired && !p) { toast(`${ex.name} требует Passphrase`, 'error'); return; }
    setBusy(true);
    try { await onSave({ key: k, secret: s, pass: p }); }
    finally { setBusy(false); }
  };
  return (
    <Modal title={`Подключить ${ex.name}`} sub={passRequired ? 'API Key + Secret + Passphrase' : 'API Key + Secret'} onClose={onClose} width={500}
      footer={<>
        <button className="btn btn-ghost" disabled={busy} onClick={onClose}>Отмена</button>
        <button className="btn btn-primary" disabled={busy} onClick={submit}>{busy ? 'Подключаем…' : 'Подключить'}</button>
      </>}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
        <div><div className="label-cap" style={{ marginBottom: 8 }}>API Key</div><input className="inp mono" value={key} onChange={e => setKey(e.target.value)} placeholder="Вставьте API key" autoFocus /></div>
        <div><div className="label-cap" style={{ marginBottom: 8 }}>API Secret</div><input className="inp mono" type="password" value={secret} onChange={e => setSecret(e.target.value)} placeholder="••••••••••••••••" /></div>
        {passRequired && (
          <div>
            <div className="label-cap" style={{ marginBottom: 8 }}>Passphrase <span style={{ color: 'var(--red)' }}>*</span></div>
            <input className="inp mono" type="password" value={pass} onChange={e => setPass(e.target.value)} placeholder={`${ex.name} passphrase`} />
            <div className="muted" style={{ fontSize: 11, marginTop: 6 }}>{ex.name} требует passphrase — это отдельная фраза которую ты задал при создании API-ключа на бирже.</div>
          </div>
        )}
        <div className="card" style={{ padding: '12px 14px', background: 'var(--amber-soft)', border: '1px solid var(--amber)', display: 'flex', gap: 10 }}>
          <Icon name="shield" size={18} style={{ color: 'var(--amber)', flex: 'none' }} />
          <span style={{ fontSize: 12.5, lineHeight: 1.5 }}>Никогда не включайте право на вывод средств. API4ATKA нужен только доступ к чтению и торговле.</span>
        </div>
      </div>
    </Modal>
  );
}

// QR-канвас компонент — рисует QR из tg:// URL через qrcode-generator (window.qrcode)
function QrCodeBox({ data, size = 220 }) {
  const ref = useRef(null);
  useEffect(() => {
    if (!ref.current || !data || !window.qrcode) return;
    try {
      const q = window.qrcode(0, 'M');
      q.addData(data);
      q.make();
      ref.current.innerHTML = q.createSvgTag({ cellSize: 4, margin: 8, scalable: true });
      const svg = ref.current.querySelector('svg');
      if (svg) { svg.setAttribute('width', String(size)); svg.setAttribute('height', String(size)); svg.style.background = '#fff'; svg.style.borderRadius = '12px'; }
    } catch (e) { /* */ }
  }, [data, size]);
  return <div ref={ref} style={{ width: size, height: size, display: 'grid', placeItems: 'center', background: '#fff', borderRadius: 12 }} />;
}

function TelegramWizard({ onClose, onConnected }) {
  const [method, setMethod] = useState('qr'); // qr | code
  const [apiId, setApiId] = useState(() => { try { return localStorage.getItem('alpha_tg_api_id') || ''; } catch { return ''; } });
  const [apiHash, setApiHash] = useState(() => { try { return localStorage.getItem('alpha_tg_api_hash') || ''; } catch { return ''; } });
  const [phone, setPhone] = useState(() => { try { return localStorage.getItem('alpha_tg_phone') || '+7'; } catch { return '+7'; } });
  // qr state
  const [qrUrl, setQrUrl] = useState('');
  const [qrStage, setQrStage] = useState('idle'); // idle | waiting | needs_password | done | error
  const [qrPwd, setQrPwd] = useState('');
  const [qrErr, setQrErr] = useState('');
  const pollRef = useRef(null);
  // code state
  const [codeStage, setCodeStage] = useState('idle'); // idle | sent | needs_password | done
  const [code, setCode] = useState('');
  const [codePwd, setCodePwd] = useState('');
  const [busy, setBusy] = useState(false);
  const toast = useToast();

  const persist = () => {
    try {
      if (apiId) localStorage.setItem('alpha_tg_api_id', apiId);
      if (apiHash) localStorage.setItem('alpha_tg_api_hash', apiHash);
      if (phone) localStorage.setItem('alpha_tg_phone', phone);
    } catch {}
  };

  const stopPoll = () => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } };
  useEffect(() => () => { stopPoll(); try { API().tgQrCancel(); } catch {} }, []);

  const startQr = async () => {
    if (busy) return;
    const aId = String(apiId).trim();
    const aHash = String(apiHash).trim();
    if (!aId || !aHash) { toast('Заполни api_id и api_hash', 'error'); return; }
    if (aHash.length !== 32) { toast(`api_hash должен быть 32 символа (сейчас ${aHash.length})`, 'error'); return; }
    persist();
    setBusy(true); setQrErr(''); setQrStage('waiting');
    try {
      const r = await API().tgQrStart(aId, aHash);
      setQrUrl(r.url || '');
      pollRef.current = setInterval(async () => {
        try {
          const p = await API().tgQrPoll();
          if (p.url && p.url !== qrUrl) setQrUrl(p.url);
          if (p.status === 'waiting') return;
          if (p.status === 'needs_password') { stopPoll(); setQrStage('needs_password'); return; }
          if (p.status === 'connected') {
            stopPoll(); setQrStage('done');
            toast('Telegram подключён', 'success');
            onConnected && onConnected();
            setTimeout(onClose, 600);
            return;
          }
          if (p.status === 'expired' || p.status === 'error') {
            stopPoll(); setQrStage('error'); setQrErr(p.error || 'QR истёк — повтори запуск');
          }
        } catch (e) { /* keep polling */ }
      }, 2500);
    } catch (e) {
      setQrStage('error'); setQrErr(e.message || String(e));
    } finally { setBusy(false); }
  };

  const submitQrPwd = async () => {
    if (busy || !qrPwd) return;
    setBusy(true);
    try {
      await API().tgQrPassword(qrPwd);
      setQrStage('done');
      toast('Telegram подключён', 'success');
      onConnected && onConnected();
      setTimeout(onClose, 600);
    } catch (e) {
      toast('Неверный пароль: ' + (e.message || e), 'error');
    } finally { setBusy(false); }
  };

  const sendCode = async (forceSms = false) => {
    if (busy) return;
    const aId = String(apiId).trim();
    const aHash = String(apiHash).trim();
    const ph = String(phone).trim();
    if (!aId || !aHash || !ph) { toast('Заполни api_id, api_hash и номер', 'error'); return; }
    if (aHash.length !== 32) { toast(`api_hash должен быть 32 символа (сейчас ${aHash.length})`, 'error'); return; }
    if (!ph.startsWith('+')) { toast('Номер должен начинаться с + (например +79001234567)', 'error'); return; }
    persist();
    setBusy(true);
    try {
      await API().tgSendCode(aId, aHash, ph, !!forceSms);
      setCodeStage('sent');
      toast(forceSms ? 'SMS отправлена · проверь телефон' : 'Код отправлен в Telegram · проверь сообщения', 'success');
    } catch (e) {
      const msg = (e.message || String(e)).slice(0, 200);
      toast('Не отправлено: ' + msg, 'error');
      console.error('[TG sendCode]', e);
    } finally { setBusy(false); }
  };
  const submitCode = async () => {
    if (busy || !code) return;
    setBusy(true);
    try {
      const r = await API().tgVerifyCode(code);
      if (r && (r.needs_password || r.need_password)) { setCodeStage('needs_password'); }
      else { setCodeStage('done'); toast('Telegram подключён', 'success'); onConnected && onConnected(); setTimeout(onClose, 600); }
    } catch (e) {
      const msg = e.message || String(e);
      if (/password/i.test(msg)) setCodeStage('needs_password');
      else toast('Неверный код: ' + msg, 'error');
    } finally { setBusy(false); }
  };
  const submitCodePwd = async () => {
    if (busy || !codePwd) return;
    setBusy(true);
    try {
      await API().tgVerifyPassword(codePwd);
      setCodeStage('done');
      toast('Telegram подключён', 'success');
      onConnected && onConnected();
      setTimeout(onClose, 600);
    } catch (e) {
      toast('Неверный пароль: ' + (e.message || e), 'error');
    } finally { setBusy(false); }
  };

  const credsOk = apiId && apiHash;
  return (
    <Modal title="Подключить Telegram" sub="MTProto · api_id / api_hash с my.telegram.org" onClose={onClose} width={560}
      footer={<button className="btn btn-ghost" onClick={onClose}>Закрыть</button>}>
      {/* Credentials block (shared) */}
      <div className="card" style={{ padding: '14px 16px', background: 'var(--bg-2)', marginBottom: 16 }}>
        <div className="row" style={{ gap: 10 }}>
          <div style={{ flex: 1 }}><div className="label-cap" style={{ marginBottom: 6 }}>api_id</div><input className="inp mono" inputMode="numeric" value={apiId} onChange={e => setApiId(e.target.value.replace(/\D/g, ''))} placeholder="1234567" /></div>
          <div style={{ flex: 2 }}><div className="label-cap" style={{ marginBottom: 6 }}>api_hash</div><input className="inp mono" value={apiHash} onChange={e => setApiHash(e.target.value.trim())} placeholder="32 hex символа" /></div>
        </div>
        <div className="muted" style={{ fontSize: 11.5, marginTop: 8 }}>
          Получи на{' '}
          <a href="https://my.telegram.org/apps" target="_blank" rel="noreferrer">my.telegram.org/apps</a>.
          Хранится только в твоём браузере + на нашем сервере для авторизации.
        </div>
      </div>

      {/* Method switch */}
      <div className="seg" style={{ width: '100%', marginBottom: 12 }}>
        <button className={method === 'qr' ? 'on' : ''} style={{ flex: 1 }} onClick={() => setMethod('qr')}>★ QR-код (рекомендуется)</button>
        <button className={method === 'code' ? 'on' : ''} style={{ flex: 1 }} onClick={() => setMethod('code')}>SMS / Telegram-код</button>
      </div>
      {method === 'code' && (
        <div className="card" style={{ padding: '10px 14px', background: 'var(--amber-soft)', border: '1px solid var(--amber)', display: 'flex', gap: 10, marginBottom: 12 }}>
          <Icon name="warning" size={16} style={{ color: 'var(--amber)', flex: 'none', marginTop: 1 }} />
          <span style={{ fontSize: 12, lineHeight: 1.4 }}>Telegram с 2024 часто блокирует SMS для сторонних приложений. Если код не приходит — используй <b>QR</b>.</span>
        </div>
      )}

      {method === 'qr' && (
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 14 }}>
          {qrStage === 'idle' && (
            <>
              <div className="muted" style={{ fontSize: 13, textAlign: 'center', maxWidth: 380 }}>
                Открой Telegram → Settings → Devices → Link Desktop Device, отсканируй QR-код.
              </div>
              <button className="btn btn-primary" disabled={!credsOk || busy} onClick={startQr}>
                <Icon name="telegram" size={15} />{busy ? 'Запускаем…' : 'Сгенерировать QR'}
              </button>
            </>
          )}
          {qrStage === 'waiting' && (
            <>
              {qrUrl ? <QrCodeBox data={qrUrl} size={240} /> : <div className="skel" style={{ width: 240, height: 240, borderRadius: 12 }} />}
              <div className="muted" style={{ fontSize: 12.5, textAlign: 'center' }}>Ждём подтверждения в Telegram…</div>
              <button className="btn btn-ghost btn-sm" onClick={() => { stopPoll(); try { API().tgQrCancel(); } catch {} setQrStage('idle'); setQrUrl(''); }}>Отмена</button>
            </>
          )}
          {qrStage === 'needs_password' && (
            <div style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 10 }}>
              <div className="label-cap">2FA облачный пароль</div>
              <input className="inp" type="password" value={qrPwd} onChange={e => setQrPwd(e.target.value)} placeholder="Cloud password" autoFocus />
              <button className="btn btn-primary" disabled={busy || !qrPwd} onClick={submitQrPwd}>{busy ? 'Проверяем…' : 'Подтвердить'}</button>
            </div>
          )}
          {qrStage === 'done' && <div style={{ color: 'var(--green)', fontWeight: 700, fontSize: 14 }}><Icon name="checkcircle" size={16} /> Подключено</div>}
          {qrStage === 'error' && (
            <>
              <div className="card" style={{ padding: '10px 14px', background: 'var(--red-soft)', border: '1px solid var(--red)', display: 'flex', gap: 10, width: '100%' }}>
                <Icon name="warning" size={18} style={{ color: 'var(--red)' }} /><span style={{ fontSize: 12.5 }}>{qrErr}</span>
              </div>
              <button className="btn btn-primary" onClick={startQr}><Icon name="refresh" size={14} />Попробовать снова</button>
            </>
          )}
        </div>
      )}

      {method === 'code' && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div><div className="label-cap" style={{ marginBottom: 6 }}>Номер телефона</div><input className="inp mono" inputMode="tel" value={phone} onChange={e => setPhone(e.target.value.trim())} placeholder="+7 900 000-00-00" /></div>
          {codeStage === 'idle' && (
            <div className="row" style={{ gap: 10 }}>
              <button className="btn btn-primary" style={{ flex: 1 }} disabled={!credsOk || !phone || busy} onClick={() => sendCode(false)}>{busy ? 'Отправляем…' : 'Прислать код в Telegram'}</button>
              <button className="btn btn-ghost" disabled={!credsOk || !phone || busy} onClick={() => sendCode(true)}>SMS</button>
            </div>
          )}
          {codeStage === 'sent' && (
            <>
              <div><div className="label-cap" style={{ marginBottom: 6 }}>Код из Telegram</div>
                <input className="inp mono" style={{ textAlign: 'center', fontSize: 22, letterSpacing: '0.2em' }} value={code} onChange={e => setCode(e.target.value.replace(/\D/g, ''))} placeholder="• • • • •" maxLength={6} autoFocus />
              </div>
              <button className="btn btn-primary" disabled={busy || !code} onClick={submitCode}>{busy ? 'Проверяем…' : 'Подтвердить код'}</button>
              <button className="btn btn-ghost btn-sm" onClick={() => { setCodeStage('idle'); setCode(''); }}>Прислать заново</button>
            </>
          )}
          {codeStage === 'needs_password' && (
            <>
              <div><div className="label-cap" style={{ marginBottom: 6 }}>2FA облачный пароль</div>
                <input className="inp" type="password" value={codePwd} onChange={e => setCodePwd(e.target.value)} placeholder="Cloud password" autoFocus />
              </div>
              <button className="btn btn-primary" disabled={busy || !codePwd} onClick={submitCodePwd}>{busy ? 'Проверяем…' : 'Подтвердить пароль'}</button>
            </>
          )}
          {codeStage === 'done' && <div style={{ color: 'var(--green)', fontWeight: 700, fontSize: 14 }}><Icon name="checkcircle" size={16} /> Подключено</div>}
        </div>
      )}
    </Modal>
  );
}

/* ---- Торговые настройки (как на мобилке; бэк общий → синхронизация авто) ---- */
function TradeSettingsPane() {
  const [tick, setTick] = useState(0);
  const [s] = useLiveData(
    async () => { const r = await fetch('/api/me/settings', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, [tick]
  );
  const [exSignals] = useLiveData(
    async () => { const r = await fetch('/api/me/exchange-signals', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, [tick]
  );
  const [busy, setBusy] = useState(false);
  const toast = useToast();
  const reload = () => setTick(t => t + 1);

  const save = async (patch, okMsg = 'Сохранено') => {
    setBusy(true);
    try {
      const r = await fetch('/api/me/settings', {
        method: 'PUT', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(patch),
      });
      if (!r.ok) throw new Error((await r.json().catch(() => ({}))).detail || r.status);
      toast(okMsg, 'success'); reload();
    } catch (e) { toast('Не сохранилось: ' + (e.message || e), 'error'); }
    setBusy(false);
  };
  const saveExSignal = async (ex, patch) => {
    setBusy(true);
    try {
      const r = await fetch(`/api/me/exchange-signals/${ex}`, {
        method: 'PUT', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(patch),
      });
      if (!r.ok) throw new Error((await r.json().catch(() => ({}))).detail || r.status);
      toast(`${ex.toUpperCase()}: сохранено`, 'success'); reload();
    } catch (e) { toast('Не сохранилось: ' + (e.message || e), 'error'); }
    setBusy(false);
  };

  if (!s) return <div className="skel" style={{ height: 300, borderRadius: 12 }} />;
  const Row = ({ title, sub, children }) => (
    <div className="row" style={{ padding: '13px 0', borderBottom: '1px solid var(--border)', gap: 14 }}>
      <div style={{ flex: 1 }}>
        <div style={{ fontWeight: 600, fontSize: 14 }}>{title}</div>
        {sub && <div className="muted" style={{ fontSize: 12, marginTop: 2 }}>{sub}</div>}
      </div>
      {children}
    </div>
  );

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      {/* Исполнение */}
      <div className="card card-pad">
        <div className="label-cap" style={{ marginBottom: 6 }}>Исполнение сигналов</div>
        <Row title="Режим" sub="auto — открывает сам · manual — ждёт подтверждения в боте/на сайте">
          <div className="seg">
            {[['auto', 'Авто'], ['manual', 'Вручную']].map(([k, l]) => (
              <button key={k} className={s.confirmation_mode === k ? 'on' : ''} disabled={busy}
                onClick={() => save({ confirmation_mode: k })}>{l}</button>
            ))}
          </div>
        </Row>
        <Row title="Порог авто-исполнения" sub="high — авто открывает только уверенные сигналы · medium — и средние тоже (карточка подтверждения приходить не будет)">
          <div className="seg">
            {[['high', 'Только high'], ['medium', 'High + medium']].map(([k, l]) => (
              <button key={k} className={(s.auto_min_confidence || 'high') === k ? 'on' : ''} disabled={busy}
                onClick={() => save({ auto_min_confidence: k })}>{l}</button>
            ))}
          </div>
        </Row>
        <div style={{ borderBottom: 'none' }}>
          <Row title="Dry-run (без реальных ордеров)" sub="Сигналы парсятся и логируются, но на биржу не идут">
            <div className={cx('tgl', s.dry_run && 'on')} onClick={() => !busy && save({ dry_run: !s.dry_run })} />
          </Row>
        </div>
      </div>

      {/* Размер и плечо */}
      <div className="card card-pad">
        <div className="label-cap" style={{ marginBottom: 6 }}>Размер позиции и плечо</div>
        <Row title="Режим размера">
          <div className="seg">
            {[['fixed_usdt', 'Фикс. USDT'], ['percent_balance', '% баланса']].map(([k, l]) => (
              <button key={k} className={s.size_mode === k ? 'on' : ''} disabled={busy}
                onClick={() => save({ size_mode: k })}>{l}</button>
            ))}
          </div>
        </Row>
        <Row title={s.size_mode === 'percent_balance' ? 'Процент от баланса' : 'Размер сделки (маржа)'}>
          <SettingNumInput
            value={s.size_mode === 'percent_balance' ? s.size_percent : s.default_position_size_usdt}
            suffix={s.size_mode === 'percent_balance' ? '%' : 'USDT'}
            disabled={busy}
            onSave={v => save(s.size_mode === 'percent_balance' ? { size_percent: v } : { default_position_size_usdt: v })}
          />
        </Row>
        <Row title="Режим плеча" sub="from_signal — берёт из сигнала · fixed — всегда одно">
          <div className="seg">
            {[['fixed', 'Фикс.'], ['from_signal', 'Из сигнала'], ['max', 'Макс.']].map(([k, l]) => (
              <button key={k} className={s.leverage_mode === k ? 'on' : ''} disabled={busy}
                onClick={() => save({ leverage_mode: k })}>{l}</button>
            ))}
          </div>
        </Row>
        <div style={{ borderBottom: 'none' }}>
          <Row title="Плечо (для fixed)">
            <SettingNumInput value={s.default_leverage} suffix="x" disabled={busy}
              onSave={v => save({ default_leverage: Math.round(v) })} />
          </Row>
        </div>
      </div>

      {/* Сигналы по биржам */}
      <div className="card card-pad">
        <div className="row between" style={{ marginBottom: 6 }}>
          <div className="label-cap">Сигналы по биржам</div>
          <div className="seg">
            {[['global', 'Глобально'], ['per_exchange', 'Раздельно']].map(([k, l]) => (
              <button key={k} className={s.signal_settings_mode === k ? 'on' : ''} disabled={busy}
                onClick={() => save({ signal_settings_mode: k })}>{l}</button>
            ))}
          </div>
        </div>
        <div className="muted" style={{ fontSize: 12, marginBottom: 10 }}>
          На какие биржи исполнять входящие сигналы. «Раздельно» — у каждой биржи свои размер/плечо/режим.
        </div>
        {(exSignals || []).map(es => (
          <div key={es.exchange}>
            <Row title={es.exchange.toUpperCase()}
              sub={es.signal_enabled ? `плечо ${es.default_leverage || '—'}x · ${es.size_mode === 'percent_balance' ? es.size_percent + '%' : (es.default_position_size_usdt || '—') + ' USDT'}` : 'сигналы выключены'}>
              <div className={cx('tgl', es.signal_enabled && 'on')}
                onClick={() => !busy && saveExSignal(es.exchange, { signal_enabled: !es.signal_enabled })} />
            </Row>
            {s.signal_settings_mode === 'per_exchange' && es.signal_enabled && (
              <div style={{ padding: '10px 0 4px', display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
                <span className="muted" style={{ fontSize: 12 }}>Плечо {es.exchange.toUpperCase()}:</span>
                <div className="seg">
                  {[['fixed', 'Фикс.'], ['from_signal', 'Из сигнала'], ['max', 'Макс.']].map(([k, l]) => (
                    <button key={k} className={(es.leverage_mode || 'fixed') === k ? 'on' : ''} disabled={busy}
                      onClick={() => saveExSignal(es.exchange, { leverage_mode: k })}>{l}</button>
                  ))}
                </div>
                {(es.leverage_mode || 'fixed') !== 'max' && (
                  <SettingNumInput value={es.default_leverage || 10} suffix="x" disabled={busy}
                    onSave={v => saveExSignal(es.exchange, { default_leverage: Math.round(v) })} />
                )}
              </div>
            )}
          </div>
        ))}
        {!(exSignals || []).length && <div className="muted" style={{ fontSize: 12.5, padding: '10px 0' }}>Подключи API-ключ биржи — она появится здесь.</div>}
      </div>
    </div>
  );
}

// Числовое поле с кнопкой-сохранением (без автосейва на каждый символ)
function SettingNumInput({ value, onSave, suffix, disabled }) {
  const [v, setV] = useState(String(value ?? ''));
  useEffect(() => { setV(String(value ?? '')); }, [value]);
  const changed = String(value ?? '') !== v && v !== '';
  return (
    <div className="row" style={{ gap: 8 }}>
      <div style={{ position: 'relative' }}>
        <input className="inp mono" style={{ width: 120 }} type="text" inputMode="decimal" value={v}
          onChange={e => setV(e.target.value.replace(',', '.'))} disabled={disabled} />
        {suffix && <span className="muted mono" style={{ position: 'absolute', right: 10, top: 10, fontSize: 12 }}>{suffix}</span>}
      </div>
      {changed && <button className="btn btn-primary btn-sm" disabled={disabled} onClick={() => onSave(parseFloat(v))}>OK</button>}
    </div>
  );
}

/* ---- Тарифы: FREE / PRO / PREMIUM ---- */
const PLANS = [
  {
    id: 'free', name: 'FREE', price: 0, accent: 'var(--text-muted)',
    tagline: 'Попробовать всё руками',
    features: [
      '1 биржа (API-ключ)',
      '2 канала-источника сигналов',
      '1 локальный бот (Grid / DCA)',
      'Журнал сделок и базовая статистика',
      'Web Push уведомления',
    ],
  },
  {
    id: 'pro', name: 'PRO', price: 24.99, accent: 'var(--accent)', hot: true,
    tagline: 'Для активного трейдинга',
    features: [
      '4 биржи · multi-account',
      '10 каналов + per-channel настройки',
      '10 локальных ботов',
      'Перенаправления сигналов (forwards)',
      'Экспорт CSV · полная аналитика',
      'TG-дайджест (скоро)',
    ],
  },
  {
    id: 'premium', name: 'PREMIUM', price: 69.99, accent: 'var(--emerald)',
    tagline: 'Максимум автоматизации',
    features: [
      'Всё из PRO без лимитов',
      'Копи-трейдинг (ранний доступ)',
      'Зеркалирование между аккаунтами',
      'Бэктест каналов (скоро)',
      'Приоритетная поддержка',
    ],
  },
];

function PlanPane({ user }) {
  const current = (user?.plan_tier || 'free').toLowerCase();
  const toast = useToast();
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      <div className="muted" style={{ fontSize: 13 }}>
        Текущий тариф: <span className="badge badge-accent" style={{ marginLeft: 4 }}>{current.toUpperCase()}</span>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
        {PLANS.map(p => {
          const isCur = p.id === current;
          return (
            <div key={p.id} className="card card-pad" style={{
              display: 'flex', flexDirection: 'column', gap: 12,
              borderColor: isCur ? p.accent : (p.hot ? 'var(--accent-ring)' : 'var(--border)'),
              position: 'relative',
            }}>
              {p.hot && !isCur && <span className="badge badge-accent" style={{ position: 'absolute', top: 14, right: 14 }}>ПОПУЛЯРНЫЙ</span>}
              {isCur && <span className="badge badge-emerald" style={{ position: 'absolute', top: 14, right: 14 }}>ТЕКУЩИЙ</span>}
              <div>
                <div style={{ fontWeight: 800, fontSize: 18, color: p.accent }}>{p.name}</div>
                <div className="muted" style={{ fontSize: 12, marginTop: 2 }}>{p.tagline}</div>
              </div>
              <div className="row" style={{ gap: 6, alignItems: 'baseline' }}>
                <span className="mono" style={{ fontSize: 30, fontWeight: 800 }}>${p.price}</span>
                <span className="muted" style={{ fontSize: 12 }}>/ мес</span>
              </div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1 }}>
                {p.features.map(f => (
                  <div key={f} className="row" style={{ gap: 8, fontSize: 12.5 }}>
                    <Icon name="check" size={13} style={{ color: p.accent, flex: 'none' }} />
                    <span style={{ color: 'var(--text-2)' }}>{f}</span>
                  </div>
                ))}
              </div>
              <button
                className={cx('btn', isCur ? 'btn-ghost' : 'btn-primary')}
                disabled={isCur}
                onClick={() => toast('Оплата картой/крипто подключается — для активации напиши в поддержку', 'info')}>
                {isCur ? 'Активен' : (p.price === 0 ? 'Перейти' : 'Активировать')}
              </button>
            </div>
          );
        })}
      </div>
      <div className="muted" style={{ fontSize: 11.5, lineHeight: 1.5 }}>
        Лимиты применяются на стороне сервера. Автосписания нет — продление вручную. Возврат в течение 7 дней если функционал не подошёл.
      </div>
    </div>
  );
}

function AccountPane({ user, onLogout }) {
  const [pwModal, setPwModal] = useState(false);
  const [oldPw, setOldPw] = useState('');
  const [newPw, setNewPw] = useState('');
  const [busy, setBusy] = useState(false);
  const toast = useToast();
  const name = user?.name || user?.email?.split('@')[0] || 'Пользователь';
  const email = user?.email || '—';
  const plan = (user?.plan_tier || 'free').toUpperCase();
  const changePw = async () => {
    if (!oldPw || !newPw || newPw.length < 6) { toast('Пароль минимум 6 символов', 'error'); return; }
    setBusy(true);
    try { await API().changePassword(oldPw, newPw); toast('Пароль изменён', 'success'); setPwModal(false); setOldPw(''); setNewPw(''); }
    catch (e) { toast('Не удалось изменить пароль: ' + (e.message || e), 'error'); }
    setBusy(false);
  };
  return (
    <div className="card card-pad">
      <div className="row" style={{ gap: 14, marginBottom: 14 }}>
        <Avatar name={name} hue={210} size={52} />
        <div style={{ flex: 1 }}><div style={{ fontWeight: 700, fontSize: 16 }}>{name}</div><div className="muted" style={{ fontSize: 13 }}>{email}</div></div>
        <span className="badge badge-accent">{plan}</span>
      </div>
      <div className="divider" />
      <div className="row between" style={{ padding: '14px 0', borderBottom: '1px solid var(--border)' }}>
        <span>Сменить пароль</span>
        <button className="btn btn-ghost btn-sm" onClick={() => setPwModal(true)}>Изменить</button>
      </div>
      <div className="row between" style={{ padding: '14px 0', borderBottom: '1px solid var(--border)' }}>
        <span>Выйти из всех сессий</span>
        <button className="btn btn-ghost btn-sm" onClick={() => { if (confirm('Выйти из аккаунта?')) onLogout && onLogout(); }}>Выйти</button>
      </div>
      <div className="row between" style={{ padding: '14px 0' }}>
        <span style={{ color: 'var(--red)' }}>Удалить аккаунт</span>
        <button className="btn btn-ghost btn-sm" style={{ color: 'var(--red)' }} onClick={() => toast('Удаление аккаунта — через поддержку', 'info')}>Удалить</button>
      </div>
      {pwModal && (
        <Modal title="Сменить пароль" onClose={() => setPwModal(false)} width={420}
          footer={<>
            <button className="btn btn-ghost" disabled={busy} onClick={() => setPwModal(false)}>Отмена</button>
            <button className="btn btn-primary" disabled={busy} onClick={changePw}>{busy ? 'Сохраняем…' : 'Сохранить'}</button>
          </>}>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
            <div><div className="label-cap" style={{ marginBottom: 6 }}>Текущий пароль</div><input className="inp" type="password" value={oldPw} onChange={e => setOldPw(e.target.value)} /></div>
            <div><div className="label-cap" style={{ marginBottom: 6 }}>Новый пароль</div><input className="inp" type="password" value={newPw} onChange={e => setNewPw(e.target.value)} placeholder="минимум 6 символов" /></div>
          </div>
        </Modal>
      )}
    </div>
  );
}

function TelegramPane({ onConnect }) {
  const [tick, setTick] = useState(0);
  // Прямой endpoint /api/me/telegram/status — отдаёт {tg_username, phone_masked, connected, ...}.
  // Поллим каждые 10с пока юзер на экране — статус сам подтянется после подключения через wizard.
  const [s] = useLiveData(
    async () => { const r = await fetch('/api/me/telegram/status', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, [tick], 10000
  );
  const toast = useToast();
  const connected = !!(s?.tg_username || s?.phone_masked || s?.connected || s?.has_session);
  const label = s?.tg_username ? '@' + s.tg_username : (s?.phone_masked || s?.phone || '');
  const phone = label;
  const reload = () => setTick(t => t + 1);
  useEffect(() => { window.__tg_reload = reload; return () => { delete window.__tg_reload; }; }, []);
  const disconnect = async () => {
    if (!confirm('Отключить Telegram? Сигналы из каналов перестанут приходить.')) return;
    try { await API().tgDisconnect(); toast('Telegram отключён', 'info'); reload(); }
    catch (e) { toast('Не удалось отключить: ' + (e.message || e), 'error'); }
  };
  // --- Бот-токен (BotFather): нужен для исполнения сигналов ---
  const [token, setToken] = useState('');
  const [busyTok, setBusyTok] = useState(false);
  const botLinked = !!s?.bot_linked;
  const botUser = s?.bot_username || '';
  const botRunning = !!s?.bot_running;
  const saveToken = async () => {
    const t = token.trim(); if (!t) return;
    setBusyTok(true);
    try {
      const r = await fetch('/api/me/telegram/bot-token', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ bot_token: t }) });
      const d = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(d.detail || 'Ошибка');
      toast('Бот привязан' + (d.bot_username ? ' · @' + d.bot_username : ''), 'success'); setToken(''); reload();
    } catch (e) { toast('Не удалось: ' + (e.message || e), 'error'); }
    setBusyTok(false);
  };
  const clearToken = async () => {
    if (!confirm('Отвязать бота? В ручном режиме сигналы перестанут исполняться.')) return;
    setBusyTok(true);
    try { await fetch('/api/me/telegram/bot-token', { method: 'DELETE', credentials: 'include' }); toast('Бот отвязан', 'info'); reload(); }
    catch (e) { toast('Ошибка: ' + (e.message || e), 'error'); }
    setBusyTok(false);
  };
  return (
    <div className="card card-pad">
      <div className="row" style={{ gap: 14, marginBottom: 12 }}>
        <div style={{ width: 44, height: 44, borderRadius: 12, background: 'var(--accent-soft)', display: 'grid', placeItems: 'center', color: 'var(--accent)' }}><Icon name="telegram" size={22} /></div>
        <div style={{ flex: 1 }}>
          <div style={{ fontWeight: 700, fontSize: 15 }}>Telegram</div>
          <div className="muted" style={{ fontSize: 12.5 }}>{connected ? `Подключён${phone ? ' · ' + phone : ''}` : 'Не подключён'}</div>
        </div>
        <StatusPill status={connected ? 'connected' : 'disconnected'} />
      </div>
      <p className="muted" style={{ fontSize: 13, lineHeight: 1.5, marginTop: 8 }}>
        Привязка Telegram-аккаунта через MTProto. После подключения API4ATKA читает сигналы из выбранных каналов и автоматически их исполняет.
      </p>
      <div className="row" style={{ gap: 10, marginTop: 14 }}>
        {connected
          ? <>
              <button className="btn btn-ghost" onClick={onConnect}><Icon name="refresh" size={14} />Переподключить</button>
              <button className="btn btn-ghost" style={{ color: 'var(--red)' }} onClick={disconnect}>Отключить</button>
            </>
          : <button className="btn btn-primary" onClick={onConnect}><Icon name="telegram" size={15} />Подключить Telegram</button>}
      </div>
      {connected && (
        <div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--border)' }}>
          <div style={{ fontWeight: 700, fontSize: 13.5, marginBottom: 4 }}>Бот для исполнения <span className="muted" style={{ fontWeight: 400 }}>· BotFather</span></div>
          <p className="muted" style={{ fontSize: 12.5, lineHeight: 1.5, marginBottom: 10 }}>
            Создай бота у @BotFather, открой его и нажми /start, затем вставь токен. Через бота приходят карточки подтверждения, и сделки исполняются. Без токена в ручном режиме сигналы не открываются.
          </p>
          {botLinked
            ? <div className="row between" style={{ gap: 10 }}>
                <div className="row" style={{ gap: 8 }}>
                  <StatusPill status={botRunning ? 'connected' : 'pending'} />
                  <span style={{ fontSize: 13.5 }}>{botUser ? '@' + botUser : 'Бот привязан'}{botRunning ? '' : ' · запускается…'}</span>
                </div>
                <button className="btn btn-ghost btn-sm" style={{ color: 'var(--red)' }} disabled={busyTok} onClick={clearToken}>Отвязать</button>
              </div>
            : <div className="row" style={{ gap: 8 }}>
                <input value={token} disabled={busyTok} placeholder="123456:ABCdef..."
                  onChange={e => setToken(e.target.value)} onKeyDown={e => e.key === 'Enter' && saveToken()}
                  style={{ flex: 1, background: 'rgba(255,255,255,.04)', border: '1px solid var(--border)', borderRadius: 10, padding: '9px 12px', color: 'var(--text)', fontFamily: 'ui-monospace,monospace', fontSize: 13, outline: 'none' }} />
                <button className="btn btn-primary" disabled={busyTok || !token.trim()} onClick={saveToken}>Привязать</button>
              </div>}
        </div>
      )}
    </div>
  );
}

function PushSettingsPane() {
  const [status, setStatus] = useState(null);
  const [busy, setBusy] = useState(false);
  const [supported, setSupported] = useState(true);
  const toast = useToast();
  const reload = async () => { try { const s = await API().pushStatus(); setStatus(s); } catch {} };
  useEffect(() => {
    (async () => {
      try { setSupported(await API().pushIsSupported()); } catch { setSupported(false); }
      reload();
    })();
  }, []);
  const enable = async () => {
    setBusy(true);
    try { await API().pushEnable(); toast('Push-уведомления включены', 'success'); await reload(); }
    catch (e) { toast('Не удалось включить: ' + (e.message || e), 'error'); }
    setBusy(false);
  };
  const disable = async () => {
    setBusy(true);
    try { await API().pushDisable(); toast('Push-уведомления выключены', 'info'); await reload(); }
    catch (e) { toast('Не удалось выключить: ' + (e.message || e), 'error'); }
    setBusy(false);
  };
  const test = async () => {
    try { await API().pushTest(); toast('Тестовое отправлено', 'success'); }
    catch (e) { toast('Тест не прошёл: ' + (e.message || e), 'error'); }
  };
  const enabled = !!status?.enabled || (status?.subscriptions || 0) > 0;
  return (
    <div className="card card-pad">
      <div className="label-cap" style={{ marginBottom: 16 }}>Push-уведомления (Web Push)</div>
      {!supported && (
        <div className="card" style={{ padding: '12px 14px', background: 'var(--amber-soft)', border: '1px solid var(--amber)', display: 'flex', gap: 10, marginBottom: 14 }}>
          <Icon name="warning" size={18} style={{ color: 'var(--amber)' }} /><span style={{ fontSize: 12.5 }}>Браузер не поддерживает Web Push.</span>
        </div>
      )}
      <div className="row between" style={{ padding: '14px 0', borderBottom: '1px solid var(--border)' }}>
        <div><div style={{ fontWeight: 600, fontSize: 14 }}>Уведомления в этом браузере</div><div className="muted" style={{ fontSize: 12.5 }}>{enabled ? `Активно (${status?.subscriptions || 0} подписок)` : 'Выключено'}</div></div>
        <div className={cx('tgl', enabled && 'on')} onClick={() => !busy && (enabled ? disable() : enable())} />
      </div>
      {enabled && (
        <div className="row between" style={{ padding: '14px 0' }}>
          <div><div style={{ fontWeight: 600, fontSize: 14 }}>Проверка</div><div className="muted" style={{ fontSize: 12.5 }}>Отправить тестовое уведомление</div></div>
          <button className="btn btn-ghost btn-sm" onClick={test} disabled={busy}>Тест</button>
        </div>
      )}
      <div className="muted" style={{ fontSize: 11.5, marginTop: 10 }}>
        События которые приходят в push: вход по сигналу, TP/SL, остановка бота, копи-трейд зеркало.
      </div>
    </div>
  );
}

function Settings({ user, onLogout }) {
  const [tab, setTab] = useState('api');
  const [tick, setTick] = useState(0);
  // Берём все credentials одним запросом — GET /me/exchange-creds → [{ exchange, label, has_creds, ... }, ...]
  const [credsRaw] = useLiveData(
    async () => { const r = await fetch('/api/me/exchange-creds', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, [tick]
  );
  const [connect, setConnect] = useState(null);
  const [tg, setTg] = useState(false);
  const toast = useToast();
  // Сгруппируем по биржам + добавим пустые карточки для нерасподключённых
  const SUPPORTED = ['weex', 'okx', 'binance', 'bybit'];
  const conns = useMemo(() => {
    const byEx = new Map();
    (credsRaw || []).forEach(c => {
      const ex = (c.exchange || '').toLowerCase();
      if (!byEx.has(ex)) byEx.set(ex, []);
      byEx.get(ex).push(c);
    });
    return SUPPORTED.map(ex => {
      const list = byEx.get(ex) || [];
      const first = list[0];
      return {
        id: ex,
        name: ex.toUpperCase(),
        status: first ? 'connected' : 'disconnected',
        balance: 0,
        key: first ? (first.api_key_mask || '••••') : null,
        perms: first ? ['Чтение', 'Торговля'] : [],
        primary: ex === 'weex',
        accountsCount: list.length,
        labels: list.map(c => c.label || 'default'),
      };
    });
  }, [credsRaw]);
  const reload = () => setTick(t => t + 1);

  // Биржа по умолчанию (active_exchange). Фолбэк — первая подключённая.
  const [meSettings] = useLiveData(
    async () => { const r = await fetch('/api/me/settings', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, [tick]
  );
  const _firstConn = ((credsRaw || []).map(c => (c.exchange || '').toLowerCase()).filter(Boolean))[0] || 'weex';
  const activeEx = (meSettings?.active_exchange || _firstConn).toLowerCase();
  const setDefaultEx = async (id) => {
    if (id === activeEx) return;
    try {
      await API().setActiveAccount(id, 'default');
      toast(`Биржа по умолчанию: ${id.toUpperCase()}`, 'success');
      setTimeout(() => window.location.reload(), 400);
    } catch (e) { toast('Не удалось: ' + (e.message || e), 'error'); }
  };

  // Реальные балансы по каждой подключённой бирже — параллельно через loadBalance(ex)
  const [exBalances, setExBalances] = useState({});
  useEffect(() => {
    let alive = true;
    const connected = conns.filter(c => c.status === 'connected');
    if (!connected.length) return;
    (async () => {
      const entries = await Promise.all(connected.map(async c => {
        try { const b = await API().loadBalance(c.id); return [c.id, b?.equity ?? null]; }
        catch { return [c.id, null]; }
      }));
      if (alive) setExBalances(Object.fromEntries(entries));
    })();
    return () => { alive = false; };
  }, [credsRaw]);

  return (
    <div className="fade-in" style={{ padding: 24, maxWidth: 880, margin: '0 auto', width: '100%', display: 'flex', flexDirection: 'column', gap: 18 }}>
      <div className="tabs" style={{ border: 'none' }}>
        {[['trading', 'Торговля'], ['api', 'API ключи'], ['telegram', 'Telegram'], ['push', 'Уведомления'], ['plan', 'Тариф'], ['account', 'Аккаунт']].map(([k, l]) => (
          <button key={k} className={tab === k ? 'on' : ''} onClick={() => setTab(k)}>{l}</button>
        ))}
      </div>

      {tab === 'trading' && <TradeSettingsPane />}

      {tab === 'plan' && <PlanPane user={user} />}

      {tab === 'api' && (<>
        <div className="card card-pad">
          <div className="label-cap" style={{ marginBottom: 6 }}>Биржа по умолчанию</div>
          <div className="muted" style={{ fontSize: 12.5, marginBottom: 12 }}>Открывается при входе и используется для исполнения сигналов. Выбор сохраняется автоматически.</div>
          <div className="seg" style={{ width: '100%' }}>
            {conns.map(c => (
              <button key={c.id} style={{ flex: 1, opacity: c.status === 'connected' ? 1 : 0.4 }}
                className={c.id === activeEx ? 'on' : ''}
                disabled={c.status !== 'connected'}
                onClick={() => c.status === 'connected' && setDefaultEx(c.id)}
                title={c.status === 'connected' ? '' : 'Сначала подключите ключ'}>{c.name}</button>
            ))}
          </div>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 16 }}>
          {conns.map(c => (
            <div key={c.id} className="card card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 12, borderColor: c.id === activeEx ? 'var(--accent-ring)' : 'var(--border)' }}>
              <div className="row between">
                <div className="row" style={{ gap: 10 }}>
                  <div style={{ width: 38, height: 38, borderRadius: 10, background: 'var(--bg-3)', display: 'grid', placeItems: 'center', fontWeight: 700 }} className="mono">{c.name[0]}</div>
                  <div><div style={{ fontWeight: 700 }}>{c.name}</div>{c.id === activeEx && <span className="badge badge-accent" style={{ marginTop: 4 }}>По умолчанию</span>}</div>
                </div>
                <StatusPill status={c.status} />
              </div>
              {c.status === 'connected' ? <>
                <div className="row between" style={{ fontSize: 12.5 }}><span className="muted">Баланс</span><span className="mono" style={{ fontWeight: 600 }}>{exBalances[c.id] != null ? fmt.usdt(exBalances[c.id]) + ' USDT' : 'загрузка…'}</span></div>
                <div className="row between" style={{ fontSize: 12.5 }}><span className="muted">Ключ</span><span className="mono">{c.key}</span></div>
                <div className="row" style={{ gap: 6 }}>{c.perms.map(p => <span key={p} className="pill pill-soft" style={{ fontSize: 10.5 }}><Icon name="check" size={10} />{p}</span>)}</div>
                <div className="divider" />
                <div className="row between"><button className="btn btn-ghost btn-sm" onClick={() => setConnect(c)}><Icon name="refresh" size={13} />Переподключить</button><button className="btn btn-ghost btn-sm" style={{ color: 'var(--red)' }} onClick={async () => { try { await API().deleteExchangeCreds(c.name.toLowerCase()); toast(`${c.name} отключена`, 'info'); reload(); } catch { toast('Не удалось отключить', 'error'); } }}>Отключить</button></div>
              </> : <>
                <div className="muted" style={{ fontSize: 12.5 }}>Подключите API-ключ {c.name}.</div>
                <button className="btn btn-primary btn-sm" style={{ marginTop: 'auto' }} onClick={() => setConnect(c)}><Icon name="key" size={14} />Подключить API</button>
              </>}
            </div>
          ))}
        </div>
      </>)}

      {tab === 'telegram' && <TelegramPane onConnect={() => setTg(true)} />}

      {tab === 'push' && <PushSettingsPane />}

      {tab === 'account' && <AccountPane user={user} onLogout={onLogout} />}

      {connect && <ConnectApiModal ex={connect} onClose={() => setConnect(null)} onSave={async (d) => {
        try {
          // sig: updateExchangeCreds(exchange, key, secret, passphrase, label)
          await API().updateExchangeCreds(connect.id || connect.name.toLowerCase(), d.key, d.secret, d.pass || '', '');
          toast(`${connect.name} подключена`, 'success'); setConnect(null); reload();
        } catch (e) { toast('Не удалось подключить: ' + (e.message || e), 'error'); }
      }} />}
      {tg && <TelegramWizard onClose={() => setTg(false)} onConnected={() => { try { window.__tg_reload && window.__tg_reload(); } catch {} reload(); }} />}
    </div>
  );
}

/* ===================== ADMIN ============================================ */
function AdminUserModal({ u, onClose, onSaved }) {
  const [role, setRole] = useState(u.role);
  const [plan, setPlan] = useState(u.plan_tier);
  const [busy, setBusy] = useState(false);
  const toast = useToast();
  const save = async () => {
    setBusy(true);
    try {
      const r = await fetch(`/api/me/admin/users/${u.id}`, {
        method: 'PATCH', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ role, plan_tier: plan }),
      });
      if (!r.ok) throw new Error((await r.json().catch(() => ({}))).detail || r.status);
      toast('Сохранено', 'success'); onSaved(); onClose();
    } catch (e) { toast('Не удалось: ' + (e.message || e), 'error'); }
    setBusy(false);
  };
  return (
    <Modal title={u.email} sub={`ID ${u.id} · регистрация ${u.created_at ? new Date(u.created_at).toLocaleDateString('ru-RU') : '—'}`} onClose={onClose} width={440}
      footer={<>
        <button className="btn btn-ghost" disabled={busy} onClick={onClose}>Отмена</button>
        <button className="btn btn-primary" disabled={busy} onClick={save}>{busy ? 'Сохраняем…' : 'Сохранить'}</button>
      </>}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
        <div>
          <div className="label-cap" style={{ marginBottom: 8 }}>Роль</div>
          <div className="seg" style={{ width: '100%' }}>
            {['user', 'admin'].map(r => <button key={r} style={{ flex: 1 }} className={role === r ? 'on' : ''} onClick={() => setRole(r)}>{r}</button>)}
          </div>
        </div>
        <div>
          <div className="label-cap" style={{ marginBottom: 8 }}>Тариф</div>
          <div className="seg" style={{ width: '100%' }}>
            {['free', 'pro', 'premium'].map(p => <button key={p} style={{ flex: 1 }} className={plan === p ? 'on' : ''} onClick={() => setPlan(p)}>{p.toUpperCase()}</button>)}
          </div>
        </div>
      </div>
    </Modal>
  );
}

function Admin() {
  const [tick, setTick] = useState(0);
  const [q, setQ] = useState('');
  const [editUser, setEditUser] = useState(null);
  const [data, loading] = useLiveData(
    async () => { const r = await fetch('/api/me/admin/overview', { credentials: 'include' }); return r.ok ? r.json() : null; },
    null, [tick], 60000
  );
  const kpi = data?.kpi || {};
  const users = (data?.users || []).filter(u =>
    !q || (u.email || '').toLowerCase().includes(q.toLowerCase()) || (u.name || '').toLowerCase().includes(q.toLowerCase())
  );
  const reload = () => setTick(t => t + 1);
  const PLAN_BADGE = { free: 'badge-amber', pro: 'badge-accent', premium: 'badge-emerald' };
  return (
    <div className="fade-in" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 16 }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16 }}>
        <Kpi label="Всего юзеров" value={kpi.users_total ?? '—'} sub={`${kpi.users_active_7d ?? 0} активны за 7д`} icon="user" accent="var(--accent)" />
        <Kpi label="Ботов всего" value={kpi.bots_total ?? '—'} sub={`${kpi.bots_running ?? 0} запущено`} icon="bot" accent="var(--emerald)" />
        <Kpi label="PRO" value={kpi.plans?.pro ?? 0} sub="платных подписок" icon="crown" accent="var(--amber)" />
        <Kpi label="PREMIUM" value={kpi.plans?.premium ?? 0} sub="топ-тариф" icon="star" accent="var(--green)" />
      </div>
      <div className="card" style={{ overflow: 'hidden' }}>
        <div className="card-pad" style={{ paddingBottom: 12 }}>
          <div className="row between">
            <SectionHead title="Пользователи" count={users.length} />
            <div style={{ position: 'relative' }}>
              <Icon name="search" size={15} style={{ position: 'absolute', left: 10, top: 11, color: 'var(--text-muted)' }} />
              <input className="inp" style={{ width: 220, paddingLeft: 32 }} placeholder="Email или имя…" value={q} onChange={e => setQ(e.target.value)} />
            </div>
          </div>
        </div>
        {loading && !data ? <div className="skel" style={{ height: 200, margin: 20, borderRadius: 12 }} /> : (
          <table className="tbl">
            <thead><tr><th>Пользователь</th><th>Роль</th><th>Тариф</th><th className="num">Ботов</th><th>Email ✓</th><th>Регистрация</th><th>Был онлайн</th></tr></thead>
            <tbody>{users.map(u => (
              <tr key={u.id} className="clickable" onClick={() => setEditUser(u)}>
                <td><div className="row" style={{ gap: 10 }}><Avatar name={u.name || u.email} hue={(u.id * 47) % 360} size={28} /><div><div className="strong">{u.name || u.email.split('@')[0]}</div><div className="muted" style={{ fontSize: 11 }}>{u.email}</div></div></div></td>
                <td><span className={cx('badge', u.role === 'admin' ? 'badge-emerald' : 'badge-accent')}>{u.role}</span></td>
                <td><span className={cx('badge', PLAN_BADGE[u.plan_tier] || 'badge-amber')}>{(u.plan_tier || 'free').toUpperCase()}</span></td>
                <td className="num mono">{u.bots}</td>
                <td>{u.is_verified ? <Icon name="checkcircle" size={15} style={{ color: 'var(--green)' }} /> : <Icon name="x" size={14} style={{ color: 'var(--text-muted)' }} />}</td>
                <td className="muted" style={{ fontSize: 12 }}>{u.created_at ? new Date(u.created_at).toLocaleDateString('ru-RU') : '—'}</td>
                <td className="muted" style={{ fontSize: 12 }}>{u.last_login_at ? _agoStr(u.last_login_at) : '—'}</td>
              </tr>
            ))}</tbody>
          </table>
        )}
        {!loading && !users.length && <Empty icon="user" title="Никого не нашли" text="Измени поисковый запрос." />}
      </div>
      {editUser && <AdminUserModal u={editUser} onClose={() => setEditUser(null)} onSaved={reload} />}
    </div>
  );
}

/* ===================== ROOT APP ========================================= */
const TITLES = {
  dashboard: 'Дашборд', bots: 'Боты', copytrade: 'Копи-трейдинг', signals: 'Сигналы',
  journal: 'Журнал', exchange: 'Биржа', spot: 'Спот', stats: 'Статистика', settings: 'Настройки', admin: 'Админ',
};

function HandoffApp({ user, setUser, onLogout }) {
  const [route, nav] = useHashRoute();
  const [collapsed, setCollapsed] = useState(false);
  const [theme, setTheme] = useState(() => { try { return localStorage.getItem('alpha_theme') || 'dark'; } catch { return 'dark'; } });

  useEffect(() => {
    document.documentElement.setAttribute('data-theme', theme);
    try { localStorage.setItem('alpha_theme', theme); } catch {}
  }, [theme]);

  const isAdmin = user && user.role === 'admin';
  const page = route.page;
  const title = TITLES[page] || 'API4ATKA';

  let Content;
  switch (page) {
    case 'dashboard': Content = <Dashboard nav={nav} user={user} />; break;
    case 'bots': Content = <Bots />; break;
    case 'copytrade': Content = <CopyTrade />; break;
    case 'signals': Content = <Signals />; break;
    case 'journal': Content = <Journal />; break;
    case 'exchange': Content = <Dashboard nav={nav} user={user} />; break; // вкладка убрана, редирект на дашборд
    case 'spot': Content = <Spot />; break;
    case 'stats': Content = <Stats />; break;
    case 'settings': Content = <Settings user={user} onLogout={onLogout} />; break;
    case 'admin': Content = isAdmin ? <Admin /> : <Dashboard nav={nav} user={user} />; break;
    default: Content = <Dashboard nav={nav} user={user} />;
  }

  return (
    <ToastHost>
      <div style={{ display: 'flex', height: '100vh' }}>
        <Sidebar route={route} nav={nav} isAdmin={isAdmin} collapsed={collapsed} setCollapsed={setCollapsed} />
        <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
          <Header title={title} theme={theme} setTheme={setTheme} nav={nav} user={user} onLogout={onLogout} />
          <main key={page} className="scroll-y" style={{ flex: 1, overflowY: 'auto', background: 'var(--bg-0)' }}>{Content}</main>
        </div>
      </div>
    </ToastHost>
  );
}

window.HandoffApp = HandoffApp;
})();
