function $(sel, ctx) { return (ctx || document).querySelector(sel); }
function $all(sel, ctx) { return Array.from((ctx || document).querySelectorAll(sel)); }

const PERIOD_HOURS = { "1h": 1, "24h": 24, "7d": 168, all: 8760 };

let api = null;
let connected = false;
let pollTimer = null;
let uptimeSeconds = 0;
let uptimeSyncedAt = Date.now();
let toggling = false;
let radarUser = null;
let telegramBotUsername = "";
let analyzeBusy = false;
let analyzeJobId = 0;
let lastSignalNotifyKey = "";
let trackingTpState = {};
let signalNotifySessionReady = false;
let trackingTpSessionReady = false;
let localTestSignals = [];
let localTestTracking = null;
let testSignalTimer = null;
let panelInForeground = true;
let panelHidden = false;
let adminCapable = false;
let memberPreviewMode = false;

const PREVIEW_MEMBER_USER = {
  telegram_id: 900000001,
  is_admin: false,
  is_guest: false,
  is_preview: true,
  has_access: true,
  has_paid_access: false,
  has_volume_access: false,
  in_community_group: true,
  first_name: "Участник",
  username: "community_preview",
  analyze_remaining: 7,
  analyze_used_today: 3,
};

function getUiUser() {
  if (memberPreviewMode) return PREVIEW_MEMBER_USER;
  return radarUser;
}

function syncPanelForeground() {
  panelInForeground =
    !panelHidden && document.visibilityState === "visible" && document.hasFocus();
}

function isPanelBackground() {
  syncPanelForeground();
  if (panelHidden) return true;
  if (document.visibilityState === "hidden") return true;
  return !panelInForeground;
}

function notifyDesktop(title, message, kind, action, forceToast) {
  if (!api || typeof api.desktop_notify !== "function") return;
  syncPanelForeground();
  const fg = panelHidden || document.visibilityState === "hidden" ? false : panelInForeground;
  call(
    "desktop_notify",
    title,
    message,
    kind || "default",
    action || "",
    fg,
    !!forceToast
  ).catch(function () {});
}

window.axonPanelFocus = function (focused) {
  panelInForeground = !!focused && !panelHidden;
};

window.axonSetPanelHidden = function (hidden) {
  panelHidden = !!hidden;
  syncPanelForeground();
};

function showToast(message, type) {
  const stack = $("#toastStack");
  const el = document.createElement("div");
  el.className = "toast" + (type === "error" ? " toast-error" : "");
  el.textContent = message;
  stack.appendChild(el);
  setTimeout(function () {
    el.classList.add("toast-out");
    setTimeout(function () { el.remove(); }, 300);
  }, 2800);
}

function showActionToast(title, actionLabel, onClick, durationMs) {
  const stack = $("#toastStack");
  if (!stack) return;
  $all(".toast-action", stack).forEach(function (old) {
    old.remove();
  });
  const el = document.createElement("div");
  el.className = "toast toast-action";
  el.innerHTML =
    '<div class="toast-title">' + escapeHtml(title) + "</div>" +
    '<button type="button" class="toast-action-btn">' + escapeHtml(actionLabel) + "</button>";
  el.querySelector(".toast-action-btn").addEventListener("click", function () {
    onClick();
    el.remove();
  });
  stack.appendChild(el);
  setTimeout(function () {
    if (!el.parentNode) return;
    el.classList.add("toast-out");
    setTimeout(function () { el.remove(); }, 300);
  }, durationMs || 15000);
}

function pushAppNotify(title, actionLabel, onClick, kind, durationMs) {
  syncPanelForeground();
  showActionToast(title, actionLabel, onClick, durationMs || 15000);
}

function deliverNotification(title, body, kind, action, inAppLabel, onInAppClick, durationMs) {
  const forceToast = kind === "signal" || kind === "tp";
  notifyDesktop(title, body, kind, action, forceToast);
  if (!isPanelBackground()) {
    showActionToast(title, inAppLabel || "Открыть", onInAppClick || function () {}, durationMs || 15000);
  }
}

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

function hideSplash() {
  var s = $("#splash");
  if (s) s.classList.add("hidden");
}

async function waitApi() {
  if (window.pywebview && window.pywebview.api) {
    api = window.pywebview.api;
    return true;
  }
  await new Promise(function (resolve) {
    var done = false;
    function finish() {
      if (done) return;
      done = true;
      if (window.pywebview && window.pywebview.api) api = window.pywebview.api;
      resolve();
    }
    window.addEventListener("pywebviewready", finish);
    var tick = setInterval(function () {
      if (window.pywebview && window.pywebview.api) {
        clearInterval(tick);
        finish();
      }
    }, 100);
    setTimeout(function () {
      clearInterval(tick);
      finish();
    }, 8000);
  });
  return !!api;
}

async function call(method) {
  if (!api) throw new Error("API панели не готов. Перезапусти run_panel.bat");
  const args = Array.prototype.slice.call(arguments, 1);
  const res = await api[method].apply(api, args);
  if (!res || !res.ok) throw new Error((res && res.error) || "Ошибка API");
  return res;
}

function formatUptime(totalSeconds) {
  const h = Math.floor(totalSeconds / 3600);
  const m = Math.floor((totalSeconds % 3600) / 60);
  return h + "ч " + m + "м";
}

function formatRelative(ts) {
  if (!ts) return "—";
  const sec = Math.max(0, Math.floor(Date.now() / 1000 - ts));
  if (sec < 60) return sec + " сек назад";
  if (sec < 3600) return Math.floor(sec / 60) + " мин назад";
  if (sec < 86400) return Math.floor(sec / 3600) + " ч назад";
  return Math.floor(sec / 86400) + " д назад";
}

function formatKyiv(ts) {
  if (!ts) return "—";
  return new Intl.DateTimeFormat("ru-RU", {
    timeZone: "Europe/Kyiv",
    day: "2-digit",
    month: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
  }).format(new Date(ts * 1000));
}

/** ID сигнала как в боте: 20260705-VANRY-0932 (Europe/Kyiv) */
function signalIdFromRow(symbol, timestamp) {
  if (!timestamp) return "";
  const sym = String(symbol || "")
    .split("/")[0]
    .replace(":", "")
    .replace(/USDT$/i, "")
    .toUpperCase()
    .trim();
  if (!sym) return "";
  const parts = new Intl.DateTimeFormat("en-GB", {
    timeZone: "Europe/Kyiv",
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    hour12: false,
  }).formatToParts(new Date(Number(timestamp) * 1000));
  function p(type) {
    const x = parts.find(function (v) {
      return v.type === type;
    });
    return x ? x.value : "";
  }
  return p("year") + p("month") + p("day") + "-" + sym + "-" + p("hour") + p("minute");
}

function resolveSignalRowId(row) {
  if (!row) return "";
  const sid = row.dataset.signalId || "";
  if (sid) return sid;
  return signalIdFromRow(row.dataset.symbol, row.dataset.ts);
}

function pctClass(pct) {
  if (pct >= 50) return "pct-hot";
  if (pct >= 20) return "pct-warm";
  return "pct-mild";
}

function pillClass(type) {
  if (type === "FLASH") return "pill-flash";
  if (type === "MEDIUM") return "pill-medium";
  return "pill-slow";
}

function setConnected(v) {
  connected = v;
  $all(".radar-indicator").forEach(function (el) {
    el.dataset.state = v ? "online" : "offline";
  });
  $all(".status-row .status-text, .status-pill .status-text").forEach(function (el) {
    el.textContent = v ? "Подключено" : "Нет связи";
  });
  $("#statusPill").classList.toggle("is-offline", !v);
  $("#bentoGrid").classList.toggle("is-offline", !v);
  $("#overviewOnlineBtns").style.display = v ? "flex" : "none";
  $("#overviewOfflineBtns").style.display = v ? "none" : "flex";
  $("#toggleTelegram").disabled = !v;
  $("#toggleHeartbeat").disabled = !v;
  applyAdminUi();
}

function renderPreview() {
  const raw = $("#greetingText").value;
  const kyiv = new Intl.DateTimeFormat("ru-RU", {
    timeZone: "Europe/Kyiv",
    hour: "2-digit",
    minute: "2-digit",
  }).format(new Date());
  const html = raw.replace(/\{kyiv_time\}/g, kyiv).replace(/\n/g, "<br>");
  $("#tgPreview").innerHTML = html;
}

function applyStatus(statusRes, settingsRes) {
  const st = statusRes.data;
  const settings = settingsRes ? settingsRes.data : {};

  uptimeSeconds = st.uptime_sec || 0;
  uptimeSyncedAt = Date.now();

  $("#metricSignals").textContent = String(st.signals_sent_session ?? "—");
  $("#metricVersion").textContent = String(st.version || "—");
  $("#metricUptime").textContent = formatUptime(uptimeSeconds);
  $("#metricScans").textContent = String(st.scans_total ?? "—");
  const aboutBot = $("#aboutBotVersion");
  if (aboutBot) aboutBot.textContent = String(st.version || "—");

  $("#lastScanVal").textContent = st.last_scan_at
    ? formatRelative(st.last_scan_at)
    : "—";
  $("#lastSignalVal").textContent = st.last_signal_at
    ? formatRelative(st.last_signal_at)
    : "—";

  if (!toggling) {
    $("#toggleTelegram").checked = !!settings.signals_enabled;
    $("#toggleHeartbeat").checked = !!settings.heartbeat_enabled;
  }
  $("#tgStatusVal").textContent = settings.signals_enabled ? "Включено" : "Выключено";
}

async function refreshOverview(quick) {
  try {
    const t0 = performance.now();
    if (quick) {
      await call("health_quick");
    } else {
      await call("health");
    }
    setConnected(true);
    if (!quick) {
      await ensureRadarSession();
    }
    const adminToken = ($("#connPass") && $("#connPass").value.trim()) || "";
    if (adminToken) {
      try {
        const statusRes = await call("status");
        const settingsRes = await call("get_settings");
        applyStatus(statusRes, settingsRes);
      } catch (adminErr) {
        applyStatus({ data: {} }, { data: {} });
      }
    } else {
      applyStatus({ data: {} }, { data: {} });
    }
    $("#latencyText").textContent = "пинг " + Math.round(performance.now() - t0) + "мс";
  } catch (e) {
    setConnected(false);
    radarUser = null;
    applyAdminUi();
    $("#metricSignals").textContent = "—";
    $("#metricVersion").textContent = "—";
    $("#metricUptime").textContent = "—";
    $("#metricScans").textContent = "—";
    $("#lastScanVal").textContent = "—";
    $("#lastSignalVal").textContent = "—";
    $("#tgStatusVal").textContent = "—";
    $("#latencyText").textContent = "";
  }
}

async function loadConfigForm() {
  const res = await call("load_config");
  const c = res.data;
  $("#connIp").value = c.host || "144.31.166.163";
  $("#connPort").value = c.port || 8765;
  $("#connPass").value = c.token || "";
  telegramBotUsername = c.telegram_bot_username || "";
  const tgBotInput = $("#connTgBot");
  if (tgBotInput) tgBotInput.value = telegramBotUsername;
  const notifySound = $("#toggleNotifySound");
  if (notifySound) notifySound.checked = c.notify_sound_enabled !== false;
  adminCapable = !!c.admin_capable;
}

async function loadAutostartState() {
  const toggle = $("#toggleAutostart");
  if (!toggle) return;
  try {
    const res = await call("get_autostart");
    const data = (res && res.data) || {};
    const sub = $("#autostartSub");
    if (data.supported === false) {
      toggle.checked = false;
      toggle.disabled = true;
      if (sub) sub.textContent = "Доступно только в установленной версии панели";
    } else {
      toggle.disabled = false;
      toggle.checked = data.enabled === true;
    }
  } catch (_) {
    /* ok — оставляем как есть */
  }
}

function isRealAdmin() {
  return !!(radarUser && radarUser.is_admin && !memberPreviewMode);
}

function isAdminUi() {
  const u = getUiUser();
  return !!(u && u.is_admin);
}

function applyAdminUi() {
  const isAdmin = isAdminUi();
  const realAdmin = isRealAdmin();
  $all(".nav-admin-only, .overview-admin-only").forEach(function (el) {
    const section = el.dataset ? el.dataset.section : "";
    if (section === "connection") {
      el.hidden = !(realAdmin || !connected);
      return;
    }
    el.hidden = !realAdmin;
  });
  const banForm = $("#banAdminForm");
  if (banForm) banForm.hidden = !isAdmin;
  const banHint = $("#banListHint");
  if (banHint) {
    banHint.textContent = isAdmin
      ? "Сверху — твои добавления (новые выше). Ниже — стандартные исключения. Любой тикер можно снять крестиком ✕."
      : "Токены, которые основной бот не сканирует. Они в списке не просто так — если проверяете их через Анализер, имейте это в виду.";
  }

  const activeNav = document.querySelector(".nav-item.active");
  if (activeNav && activeNav.classList.contains("nav-admin-only") && !isAdmin) {
    const overviewNav = document.querySelector('.nav-item[data-section="overview"]');
    if (overviewNav) overviewNav.click();
  }
}

function isRadarLoggedIn() {
  if (memberPreviewMode) return true;
  return !!(radarUser && radarUser.telegram_id && !radarUser.is_guest);
}

function hasLiveSignalAccess() {
  const uiUser = getUiUser();
  return !!(uiUser && (uiUser.has_access || uiUser.is_admin));
}

let signalsOutcomesView = false;

function canViewSignalInsights(signalId) {
  if (hasLiveSignalAccess() || isTestSignalId(signalId)) return true;
  return signalsOutcomesView;
}

function signalsBlockedMessage() {
  if (!isRadarLoggedIn()) {
    return "Войдите в аккаунт: вкладка «Профиль» → «Войти через сайт».";
  }
  return "Вступите в AXON Community, чтобы получать сигналы (подписка или объём BingX). Проверка — во вкладке «Профиль».";
}

function signalsWaitingMessage() {
  return "Ждите сигналов от бота — когда появится новый SHORT, увидите здесь и в уведомлении Windows.";
}

function signalsOutcomesEmptyMessage() {
  return "За выбранный период нет завершённых сигналов. История отработки появится после закрытия сопровождения.";
}

function setSignalsEmptyMessage(text) {
  const empty = $("#signalsEmpty");
  if (!empty) return;
  empty.style.display = "flex";
  empty.innerHTML = "<span>" + escapeHtml(text) + "</span>";
}

function enterMemberPreview() {
  memberPreviewMode = true;
  applyAdminUi();
  renderProfileUi();
  renderSignals();
  renderBanList();
  showToast("Режим просмотра: участник community");
}

function exitMemberPreview() {
  memberPreviewMode = false;
  applyAdminUi();
  renderProfileUi();
  renderSignals();
  renderBanList();
  showToast("Вышли из режима просмотра");
}

function profileAccessLabel(user) {
  if (!user) return "—";
  if (user.is_preview) return "Просмотр · участник";
  if (user.is_admin) return "Админ";
  if (user.has_paid_access) return "Community · подписка";
  if (user.has_volume_access) return "Community · объём BingX";
  if (user.has_access) return "Community";
  if (user.telegram_id) return "Telegram · без community";
  return "Гость";
}

function profileAnalyzeLabel(user) {
  if (!user) return "";
  if (user.has_access || user.is_admin) return "Анализер без лимита";
  if (user.analyze_remaining != null) {
    return "Анализер: " + user.analyze_remaining + " из " + (user.analyze_limit || 10) + " сегодня";
  }
  return "Анализер: 10 запросов в день";
}

function profileAnalyzerTableValue(user) {
  if (!user) return "—";
  if (user.has_access || user.is_admin) return "без лимита";
  if (user.analyze_remaining != null) {
    return user.analyze_remaining + " из " + (user.analyze_limit || 10) + " сегодня";
  }
  return "10 в день";
}

function setProfileBlockVisible(el, visible) {
  if (!el) return;
  el.hidden = !visible;
  el.classList.toggle("profile-hidden", !visible);
}

function profileRefreshToast(user) {
  if (!user) return "Сессия не найдена";
  if (user.is_admin) return "Админ · полный доступ";
  if (user.has_paid_access) return "Подписка активна · Анализер без лимита";
  if (user.has_volume_access) return "Доступ за объём BingX · Анализер без лимита";
  if (user.has_access) return "Community-доступ активен";
  return "Community-доступ не найден — проверь подписку или BingX на axon-trade.com";
}

function renderProfileUi() {
  const status = $("#profileStatus");
  const meta = $("#profileMeta");
  const details = $("#profileDetails");
  const hint = $("#analyzerLimitHint");
  const loginBlock = $("#profileLoginBlock");
  const actionsBlock = $("#profileActions");
  const refreshBtn = $("#btnRadarRefresh");
  const logoutBtn = $("#btnRadarLogout");
  const previewBanner = $("#profilePreviewBanner");
  const btnPreviewMember = $("#btnPreviewMember");
  const btnExitPreview = $("#btnExitPreview");
  if (!status || !meta) return;

  if (!radarUser) {
    status.textContent = "Не подключено к серверу";
    meta.textContent = "";
    if (details) details.hidden = true;
    setProfileBlockVisible(loginBlock, true);
    setProfileBlockVisible(actionsBlock, false);
    if (refreshBtn) refreshBtn.hidden = true;
    if (logoutBtn) logoutBtn.hidden = true;
    if (previewBanner) previewBanner.hidden = true;
    if (btnPreviewMember) btnPreviewMember.hidden = true;
    if (btnExitPreview) btnExitPreview.hidden = true;
    return;
  }

  const uiUser = getUiUser();
  const loggedIn = isRadarLoggedIn();
  status.textContent = profileAccessLabel(uiUser);

  const metaParts = [];
  if (loggedIn) {
    if (uiUser.first_name) metaParts.push(uiUser.first_name);
    if (uiUser.username) metaParts.push("@" + uiUser.username);
  }
  meta.textContent = metaParts.filter(Boolean).join(" · ");

  if (previewBanner) previewBanner.hidden = !memberPreviewMode;

  if (details) {
    if (loggedIn || radarUser.is_guest) {
      details.hidden = false;
      const rows = [];
      if (loggedIn) {
        rows.push(["Имя", uiUser.first_name || "—"]);
        rows.push(["Telegram", uiUser.username ? "@" + uiUser.username : "id " + uiUser.telegram_id]);
      } else {
        rows.push(["Режим", "Гость"]);
      }
      rows.push(["Community", uiUser.has_access ? "активен" : "нет"]);
      if (loggedIn) {
        rows.push(["Подписка", uiUser.has_paid_access ? "да" : "нет"]);
        rows.push(["Объём BingX", uiUser.has_volume_access ? "достаточно" : "нет"]);
        rows.push(["Группа community", uiUser.in_community_group ? "в группе" : "не в группе"]);
      }
      rows.push(["Анализер", profileAnalyzerTableValue(uiUser)]);
      details.innerHTML = rows.map(function (row) {
        const isAnalyzer = row[0] === "Анализер";
        const unlimited = isAnalyzer && (uiUser.has_access || uiUser.is_admin);
        return (
          '<div class="profile-kv-row">' +
          '<span class="profile-kv-label">' + escapeHtml(row[0]) + "</span>" +
          '<span class="profile-kv-value' + (unlimited ? " is-unlimited" : "") + '">' + escapeHtml(row[1]) + "</span>" +
          "</div>"
        );
      }).join("");
    } else {
      details.hidden = true;
      details.innerHTML = "";
    }
  }

  setProfileBlockVisible(loginBlock, !loggedIn);
  setProfileBlockVisible(actionsBlock, loggedIn);
  if (refreshBtn) refreshBtn.hidden = !loggedIn || memberPreviewMode;
  if (logoutBtn) logoutBtn.hidden = !loggedIn || memberPreviewMode;
  if (btnExitPreview) btnExitPreview.hidden = !memberPreviewMode;
  if (btnPreviewMember) btnPreviewMember.hidden = loggedIn || !adminCapable;

  if (hint) {
    hint.textContent = uiUser.has_access || uiUser.is_admin
      ? "Community — Анализер без лимита"
      : "Гостям — 10 запросов Анализера в день";
  }
}

async function logoutRadar() {
  try {
    memberPreviewMode = false;
    await call("save_radar_session", "");
    const res = await call("radar_auth_guest");
    radarUser = res.data || radarUser;
    applyAdminUi();
    renderProfileUi();
    renderSignals();
    renderBanList();
    showToast("Вышли из аккаунта · гостевой режим");
  } catch (e) {
    showToast(String(e.message || e), "error");
  }
}

function mountTelegramLogin() {
  const mount = $("#telegramLoginMount");
  if (!mount || !telegramBotUsername) {
    if (mount) mount.innerHTML = '<p class="hint">Legacy-виджет отключён. Используй вход через сайт выше.</p>';
    return;
  }
  mount.innerHTML = "";
  const script = document.createElement("script");
  script.async = true;
  script.src = "https://telegram.org/js/telegram-widget.js?22";
  script.setAttribute("data-telegram-login", telegramBotUsername.replace(/^@/, ""));
  script.setAttribute("data-size", "large");
  script.setAttribute("data-onauth", "onTelegramAuth(user)");
  script.setAttribute("data-request-access", "write");
  mount.appendChild(script);
}

async function authViaSite() {
  const btn = $("#btnSiteAuth");
  if (btn) {
    btn.disabled = true;
    btn.textContent = "Жду подтверждение на сайте…";
  }
  try {
    const res = await call("start_site_auth");
    radarUser = res.data || radarUser;
    if (radarUser && radarUser.is_admin) adminCapable = true;
    memberPreviewMode = false;
    applyAdminUi();
    renderProfileUi();
    renderBanList();
    renderSignals();
    showToast("Вошли через axon-trade.com · " + profileRefreshToast(radarUser));
  } catch (e) {
    showToast(String(e.message || e), "error");
  } finally {
    if (btn) {
      btn.disabled = false;
      btn.textContent = "Войти через сайт";
    }
  }
}

function bindAnalyzerReportActions() {
  const report = $("#analyzerReport");
  if (!report) return;
  report.querySelectorAll(".ar-top-pick").forEach(function (btn) {
    btn.addEventListener("click", function () {
      const ticker = btn.getAttribute("data-ticker") || btn.textContent || "";
      const input = $("#analyzerQuery");
      if (input) input.value = ticker;
      runAnalyzer();
    });
  });
}

function renderAnalyzeReport(data) {
  const wrap = $("#analyzerReportWrap");
  const report = $("#analyzerReport");
  if (!wrap || !report) return;
  wrap.hidden = false;
  report.innerHTML = data.report_html || "<p>Нет данных</p>";
  if (data.first_signal && data.first_signal.report_html) {
    report.innerHTML += '<hr class="analyzer-sep">' + data.first_signal.report_html;
  } else if (data.first_signal_note) {
    report.innerHTML += '<p class="hint">' + escapeHtml(data.first_signal_note) + "</p>";
  }
  bindAnalyzerReportActions();
}

function createTestSignal() {
  const now = Math.floor(Date.now() / 1000);
  return {
    signal_id: "test-" + now,
    symbol: "PEPE/USDT",
    timestamp: now,
    pump_type: "SHORT",
    result: "активен",
    change_24h: 34.2,
    volume_24h: 128500000,
    is_test: true,
  };
}

function mergeSignalRows(apiRows) {
  const testIds = new Set(localTestSignals.map(function (s) { return s.signal_id; }));
  const filtered = (apiRows || []).filter(function (s) {
    return !testIds.has(s.signal_id);
  });
  return localTestSignals.concat(filtered);
}

function signalNotifyText(s) {
  const sym = String(s.symbol || "?").replace("/USDT", "");
  const title = "Новый сигнал · " + sym;
  let body = s.pump_type || "SHORT";
  if (s.change_24h != null) body += " · +" + Number(s.change_24h).toFixed(1) + "%";
  else if (s.result && s.result !== "—") body += " · " + s.result;
  return { title: title, body: body, sym: sym };
}

function resetNotifySessionBaseline() {
  signalNotifySessionReady = false;
  trackingTpSessionReady = false;
  lastSignalNotifyKey = "";
  trackingTpState = {};
}

function notifyTpHit(track, newTp) {
  const sym = String(track.symbol || track.signal_id || "?").replace("/USDT", "");
  const title = "TP" + newTp + " · " + sym;
  const body = String(track.signal_id || sym);
  deliverNotification(
    title,
    body,
    "tp",
    "signals",
    "Открыть сигнал",
    function () {
      openSignalsSection();
      if (track.signal_id) {
        highlightSignalRow(track.signal_id);
        openSignalModal(track.signal_id);
      }
    },
    15000
  );
}

function notifyNewSignal(s) {
  const n = signalNotifyText(s);
  deliverNotification(
    n.title,
    n.body,
    "signal",
    "signals",
    "Открыть сигналы",
    function () {
      openSignalsSection();
      highlightSignalRow(s.signal_id);
    },
    15000
  );
}

function highlightSignalRow(signalId) {
  if (!signalId) return;
  setTimeout(function () {
    const row = document.querySelector('.signal-history-row[data-signal-id="' + signalId + '"]');
    if (!row) return;
    row.classList.add("signal-row-highlight");
    row.scrollIntoView({ behavior: "smooth", block: "center" });
    setTimeout(function () { row.classList.remove("signal-row-highlight"); }, 3200);
  }, 120);
}

function renderTestSignalDetailPanel(s) {
  if (!s) return '<p class="hint">Тестовый сигнал недоступен.</p>';
  const sym = String(s.symbol || "?").replace("/USDT", "");
  return (
    '<div class="signal-detail-inner signal-detail-test">' +
    '<div class="signal-detail-head">' +
    '<span class="mono signal-detail-id">' + escapeHtml(s.signal_id) + "</span>" +
    '<span class="pill pill-test">Тест</span>' +
    "</div>" +
    '<div class="signal-stats signal-stats-compact">' +
    '<div class="signal-stat"><div class="signal-stat-label">Тикер</div><div class="signal-stat-value">' + escapeHtml(sym) + "</div></div>" +
    '<div class="signal-stat"><div class="signal-stat-label">Тип</div><div class="signal-stat-value">' + escapeHtml(s.pump_type || "SHORT") + "</div></div>" +
    '<div class="signal-stat"><div class="signal-stat-label">24h</div><div class="signal-stat-value pct-up">+' + Number(s.change_24h || 0).toFixed(1) + "%</div></div>" +
    '<div class="signal-stat"><div class="signal-stat-label">Объём</div><div class="signal-stat-value">$' + (Number(s.volume_24h || 0) / 1e6).toFixed(1) + "M</div></div>" +
    "</div>" +
    '<p class="hint">Демо-строка для проверки уведомлений. График и сопровождение недоступны.</p>' +
    "</div>"
  );
}

function removeTestSignal(signalId) {
  localTestSignals = localTestSignals.filter(function (s) {
    return s.signal_id !== signalId;
  });
  renderSignals();
  showToast("Тестовый сигнал удалён");
}

function fireTestSignal() {
  if (testSignalTimer) {
    showToast("Тест уже запланирован — подожди");
    return;
  }
  const delay = 3000 + Math.floor(Math.random() * 2000);
  showToast("Тестовый сигнал через " + Math.round(delay / 1000) + " сек — можно свернуть окно");
  testSignalTimer = setTimeout(function () {
    testSignalTimer = null;
    const s = createTestSignal();
    localTestSignals.unshift(s);
    if (localTestSignals.length > 5) localTestSignals.length = 5;
    notifyNewSignal(s);
    renderSignals();
  }, delay);
}

function handleNotifyAction(action) {
  panelHidden = false;
  syncPanelForeground();
  if (action === "analyzer") {
    openAnalyzerSection();
    return;
  }
  openSignalsSection();
  if (localTestSignals.length) {
    highlightSignalRow(localTestSignals[0].signal_id);
  }
}

window.axonNotifyAction = handleNotifyAction;

async function pollNotifyUiActions() {
  if (!api) return;
  try {
    const hidRes = await call("is_panel_hidden");
    const hidden = hidRes && hidRes.data && hidRes.data.hidden;
    if (typeof hidden === "boolean" && hidden !== panelHidden) {
      panelHidden = hidden;
      syncPanelForeground();
    }
  } catch (_) {}
  try {
    const actRes = await call("pop_pending_notify_action");
    const action = actRes && actRes.data && actRes.data.action;
    if (action) {
      handleNotifyAction(action);
    }
  } catch (_) {}
}

function isTestSignalId(signalId) {
  return String(signalId || "").indexOf("test-") === 0;
}

function openSignalsSection() {
  const nav = document.querySelector('.nav-item[data-section="signals"]');
  if (nav) nav.click();
}

function openAnalyzerSection() {
  const nav = document.querySelector('.nav-item[data-section="analyzer"]');
  if (nav) nav.click();
}

function isTopAnalyzeQuery(q) {
  const raw = String(q || "").trim().toLowerCase();
  return raw === "/top" || raw === "top" || raw === "topcoins" || raw === "/topcoins";
}

async function runAnalyzer() {
  const q = ($("#analyzerQuery") && $("#analyzerQuery").value || "").trim();
  if (!q) {
    showToast("Введи тикер", "error");
    return;
  }
  if (analyzeBusy) {
    showToast("Уже считаю — можно переключать вкладки, пришлю уведомление");
    return;
  }

  const btn = $("#btnAnalyze");
  const jobId = ++analyzeJobId;
  analyzeBusy = true;
  const topQuery = isTopAnalyzeQuery(q);

  if (btn) {
    btn.textContent = topQuery ? "Сканирую рынок…" : "Считаю…";
  }
  showToast(
    topQuery
      ? "Ищем топ памп-монет — можно переключать вкладки"
      : "Анализер считает — можно переключать вкладки"
  );

  try {
    const res = await call("radar_analyze", q);
    if (jobId !== analyzeJobId) return;
    const data = res.data || {};
    radarUser = Object.assign({}, radarUser || {}, {
      analyze_remaining: data.analyze_remaining,
      analyze_used_today: data.analyze_used_today,
    });
    renderProfileUi();
    renderAnalyzeReport(data);

    const toastTitle = topQuery
      ? "Топ памп-монет готов"
      : "Анализер готов: " + q.toUpperCase();
    syncPanelForeground();
    deliverNotification(
      toastTitle,
      topQuery ? "Топ памп-монет — открой вкладку Анализер" : q.toUpperCase() + " — открой вкладку Анализер",
      "analyzer",
      "analyzer",
      "Открыть результат",
      function () {
        openAnalyzerSection();
        renderAnalyzeReport(data);
      },
      topQuery ? 15000 : 10000
    );
  } catch (e) {
    if (jobId === analyzeJobId) {
      const msg = String(e.message || e);
      if (/429|Too Many Requests|попытки.*закончи|лимит.*исчерп/i.test(msg)) {
        if (radarUser) {
          radarUser = Object.assign({}, radarUser, { analyze_remaining: 0 });
          renderProfileUi();
        }
        showToast(
          "Ваши попытки Анализера на сегодня закончились. Завтра лимит обновится, или войди с community-подпиской.",
          "error"
        );
      } else {
        showToast(msg, "error");
      }
    }
  } finally {
    if (jobId === analyzeJobId) {
      analyzeBusy = false;
      if (btn) btn.textContent = "Анализировать";
    }
  }
}

window.onTelegramAuth = async function (user) {
  try {
    const res = await call("radar_auth_telegram", user);
    radarUser = res.data || radarUser;
    applyAdminUi();
    renderProfileUi();
    renderSignals();
    showToast("Вошли через Telegram");
  } catch (e) {
    showToast(String(e.message || e), "error");
  }
};

async function ensureRadarSession() {
  if (!connected) return;
  let hasSession = false;
  try {
    const cfg = await call("load_config");
    hasSession = !!(cfg.data && cfg.data.radar_session);
    if (cfg.data && cfg.data.admin_capable) adminCapable = true;
  } catch (_) {}
  try {
    const res = await call("radar_me");
    radarUser = res.data || radarUser;
    if (radarUser && radarUser.is_admin) adminCapable = true;
    void call("telemetry_event", "open");
  } catch (e) {
    if (!hasSession) {
      try {
        const guest = await call("radar_auth_guest");
        radarUser = guest.data || radarUser;
      } catch (err) {
        showToast(String(err.message || err), "error");
      }
    }
  }
  applyAdminUi();
  renderProfileUi();
}

let signalChart = null;
let signalChartSeries = null;
let signalMarkerSeries = null;
let tpMarkerSeries = null;
let signalChartPriceLines = [];
let chartLivePriceLine = null;
let signalChartCandles = [];
let signalChartDetail = null;
let signalChartSignalTime = 0;
let signalModalId = null;
let signalLivePoll = null;
let signalCandlePoll = null;
let signalModalBingxUrl = "";
let signalChartTf = "1m";
let measureMode = false;
let rulerDragging = false;
let rulerStartPrice = null;
let rulerStartX = null;
let rulerDragBound = false;
let signalChartMarkerHits = [];
let signalChartClickBound = false;
let signalChartLastDetail = null;

function formatKyivChartTime(time) {
  var sec = time;
  if (time && typeof time === "object" && time.year) {
    sec = Math.floor(Date.UTC(time.year, time.month - 1, time.day) / 1000);
  }
  return new Date(sec * 1000).toLocaleString("ru-UA", {
    timeZone: "Europe/Kyiv",
    hour: "2-digit",
    minute: "2-digit",
    hour12: false,
  });
}

function formatKyivChartDate(time) {
  var sec = time;
  if (time && typeof time === "object" && time.year) {
    sec = Math.floor(Date.UTC(time.year, time.month - 1, time.day) / 1000);
  }
  return new Date(sec * 1000).toLocaleString("ru-UA", {
    timeZone: "Europe/Kyiv",
    day: "2-digit",
    month: "2-digit",
  });
}

function fmtPrice(v) {
  if (v == null || v === "" || Number.isNaN(Number(v))) return "—";
  const n = Number(v);
  if (Math.abs(n) >= 1) return n.toFixed(4);
  if (Math.abs(n) >= 0.01) return n.toFixed(6);
  return n.toFixed(8);
}

const SIGNAL_REF_PRICE_PREFIX = "axon-ref-prices-";

function pricesDiffer(a, b) {
  if (a == null || b == null || Number.isNaN(Number(a)) || Number.isNaN(Number(b))) return false;
  return Math.abs(Number(a) - Number(b)) > 1e-9;
}

function applySignalReferencePrices(d, signalId) {
  if (!d || !signalId) return d;
  const ref = d.reference_prices;
  if (ref) {
    if (ref.signal_price != null && !Number.isNaN(Number(ref.signal_price))) {
      d.signal_price = Number(ref.signal_price);
    }
    if (ref.send_price != null && !Number.isNaN(Number(ref.send_price))) {
      d.send_price = Number(ref.send_price);
    }
  }

  const key = SIGNAL_REF_PRICE_PREFIX + String(signalId);
  let cached = null;
  try {
    const raw = localStorage.getItem(key);
    if (raw) cached = JSON.parse(raw);
  } catch (_) {
    /* ignore */
  }

  const sig = Number(d.signal_price);
  const send = Number(d.send_price);
  if (cached && pricesDiffer(cached.signal_price, cached.send_price)) {
    d.signal_price = Number(cached.signal_price);
    d.send_price = Number(cached.send_price);
    return d;
  }
  if (pricesDiffer(sig, send)) {
    try {
      localStorage.setItem(
        key,
        JSON.stringify({ signal_price: sig, send_price: send })
      );
    } catch (_) {
      /* ignore */
    }
  }
  return d;
}

function displaySymbolFromDetail(d, signalId) {
  if (!d) return "—";
  if (d.display_symbol) return String(d.display_symbol);
  if (d.symbol) {
    return String(d.symbol).split("/")[0].replace(":USDT", "").replace(":USDT", "").toUpperCase();
  }
  const m = String(signalId || d.signal_id || "").match(/^\d{8}-(.+)-\d{4}$/);
  return m ? m[1] : "—";
}

function drawReferencePriceLines(d, pf) {
  const sig = Number(d.signal_price);
  const send = Number(d.send_price);
  if (!Number.isNaN(sig)) {
    addChartPriceLine(signalChartSeries, sig, "#F2F2F0", "Сигнал", true);
  }
  if (!Number.isNaN(send)) {
    addChartPriceLine(signalChartSeries, send, "#8F8F8F", "Отпр.", true);
  }
}

function chartPriceFormat(values) {
  const nums = values
    .filter(function (v) {
      return v != null && !Number.isNaN(Number(v));
    })
    .map(Number);
  if (!nums.length) {
    return { precision: 6, minMove: 0.000001 };
  }
  const ref = Math.min.apply(null, nums);
  if (ref >= 1000) return { precision: 2, minMove: 0.01 };
  if (ref >= 1) return { precision: 4, minMove: 0.0001 };
  if (ref >= 0.1) return { precision: 5, minMove: 0.00001 };
  if (ref >= 0.01) return { precision: 6, minMove: 0.000001 };
  return { precision: 8, minMove: 0.00000001 };
}

function collectChartPrices(d, candles) {
  const prices = [d.signal_price, d.send_price, d.live_price];
  const z1 = d.zone1;
  const z2 = d.zone2;
  if (z1) {
    prices.push(z1.sl, z1.entry_low, z1.entry_high);
    (z1.tps || []).forEach(function (tp) {
      prices.push(tp);
    });
  }
  if (z2 && zone2ShouldShow(d)) {
    prices.push(z2.sl, z2.entry_low, z2.entry_high);
    (z2.tps || []).forEach(function (tp) {
      prices.push(tp);
    });
  }
  (candles || []).forEach(function (c) {
    prices.push(c.low, c.high, c.close);
  });
  return prices;
}

function snapMarkerTime(signalTime, candles) {
  if (!signalTime || !candles || !candles.length) return signalTime;
  for (let i = 0; i < candles.length; i += 1) {
    if (candles[i].time >= signalTime) return candles[i].time;
  }
  return candles[candles.length - 1].time;
}

function setSignalTfActive(tf) {
  signalChartTf = tf || "1m";
  $all(".signal-tf", $("#signalTfBar")).forEach(function (btn) {
    btn.classList.toggle("active", btn.dataset.tf === signalChartTf);
  });
}

function deviationToneClass(v) {
  const n = Number(v);
  if (n > 0) return "positive";
  if (n < 0) return "negative";
  return "";
}

function fmtPct(v) {
  if (v == null || Number.isNaN(Number(v))) return "—";
  const n = Number(v);
  return (n > 0 ? "+" : "") + n.toFixed(2) + "%";
}

function pctBetween(from, to) {
  if (!from || Number.isNaN(Number(from)) || Number.isNaN(Number(to))) return null;
  return ((Number(to) - Number(from)) / Number(from)) * 100;
}

function zoneAllTpsHit(zone) {
  if (!zone) return false;
  const tps = zone.tps || [];
  return tps.length > 0 && Number(zone.tp_hit || 0) >= tps.length;
}

function zone2ShouldShow(d) {
  if (!d || !d.zone2) return false;
  if (d.zone2_visible) return true;
  if (d.zone2.activated) return true;
  const z1 = d.zone1;
  if (z1 && z1.sl_hit) return true;
  return false;
}

function signalSetupClosed(d) {
  if (!d) return false;
  if (d.completed) return true;
  const z1 = d.zone1;
  if (z1 && z1.bu_hit) return true;
  const z2Active = d.zone2 && zone2ShouldShow(d) && !d.zone2_disabled;
  if (z2Active) {
    const z2 = d.zone2;
    if (z2.bu_hit || zoneAllTpsHit(z2) || z2.sl_hit) return true;
    return false;
  }
  if (z1 && zoneAllTpsHit(z1)) return true;
  return false;
}

function renderZoneCard(zone, signalPrice) {
  if (!zone) return "";
  const activated = zone.activated !== false;
  const tpHit = activated ? Number(zone.tp_hit || 0) : 0;
  const allTps = zoneAllTpsHit(zone);
  const rows = [];
  if (tpHit < 1) {
    rows.push(["Набор", fmtPrice(zone.entry_low) + " – " + fmtPrice(zone.entry_high)]);
  }
  if (allTps) {
    /* сетап закрыт — SL→БУ не показываем */
  } else if (tpHit >= 1 && signalPrice != null) {
    rows.push(["SL → БУ", fmtPrice(signalPrice) + " (сигнальная)"]);
  } else {
    rows.push(["SL", fmtPrice(zone.sl)]);
  }
  (zone.tps || []).forEach(function (tp, i) {
    const hit = i < tpHit;
    rows.push([(hit ? "TP" + (i + 1) + " ✓" : "TP" + (i + 1)), fmtPrice(tp)]);
  });
  return (
    '<div class="signal-zone-card">' +
    '<div class="signal-zone-title">' + zone.label + "</div>" +
    rows
      .map(function (r) {
        const hit = r[0].indexOf("✓") >= 0 ? " hit" : "";
        return (
          '<div class="signal-zone-row' +
          hit +
          '"><span>' +
          r[0] +
          "</span><span>" +
          r[1] +
          "</span></div>"
        );
      })
      .join("") +
    "</div>"
  );
}

function renderSignalStats(d) {
  const stats = [
    ["Сигнальная цена", fmtPrice(d.signal_price), ""],
    ["Цена отправки", fmtPrice(d.send_price), ""],
    ["Текущая цена", fmtPrice(d.live_price), ""],
    [
      "Отклонение",
      (d.deviation_signal_pct > 0 ? "+" : "") + Number(d.deviation_signal_pct || 0).toFixed(2) + "%",
      deviationToneClass(-Number(d.deviation_signal_pct || 0)),
    ],
  ];
  return stats
    .map(function (s) {
      return (
        '<div class="signal-stat">' +
        '<div class="signal-stat-label">' +
        s[0] +
        "</div>" +
        '<div class="signal-stat-value ' +
        s[2] +
        '">' +
        s[1] +
        "</div></div>"
      );
    })
    .join("");
}

function renderSignalDetailPanel(d) {
  if (!d || !d.signal_id) {
    return '<p class="hint">Параметры сигнала недоступны.</p>';
  }
  let levelsHtml = renderZoneCard(d.zone1, d.signal_price);
  if (zone2ShouldShow(d)) {
    levelsHtml += renderZoneCard(d.zone2, d.signal_price);
  } else if (d.zone2 && d.zone2_disabled && !d.zone2.activated) {
    levelsHtml +=
      '<div class="signal-zone-card"><div class="signal-zone-title">Зона 2</div><div class="signal-zone-row"><span>Статус</span><span>отключена (Z1 TP)</span></div></div>';
  }
  const result = d.result_label || d.outcome || "";
  return (
    '<div class="signal-detail-inner">' +
    '<div class="signal-detail-head">' +
    '<span class="mono signal-detail-id">' + escapeHtml(d.signal_id) + "</span>" +
    (result ? '<span class="pill signal-detail-result">' + escapeHtml(result) + "</span>" : "") +
    "</div>" +
    '<div class="signal-stats signal-stats-compact">' + renderSignalStats(d) + "</div>" +
    '<div class="signal-levels signal-levels-compact">' + levelsHtml + "</div>" +
    "</div>"
  );
}

async function toggleSignalRowDetail(signalId, mainRow, expandBtn) {
  if (!canViewSignalInsights(signalId)) {
    showToast(signalsBlockedMessage(), "error");
    return;
  }
  const detailRow = mainRow.nextElementSibling;
  if (!detailRow || !detailRow.classList.contains("signal-detail-row")) return;

  const isOpen = detailRow.classList.contains("is-open");
  if (isOpen) {
    detailRow.hidden = true;
    detailRow.classList.remove("is-open");
    if (expandBtn) expandBtn.classList.remove("is-open");
    mainRow.classList.remove("is-expanded");
    return;
  }

  $all(".signal-detail-row.is-open", mainRow.parentNode).forEach(function (openRow) {
    openRow.hidden = true;
    openRow.classList.remove("is-open");
    const prev = openRow.previousElementSibling;
    if (prev) prev.classList.remove("is-expanded");
    const prevBtn = prev ? prev.querySelector(".signal-expand-btn") : null;
    if (prevBtn) prevBtn.classList.remove("is-open");
  });

  const panel = detailRow.querySelector(".signal-detail-panel");
  if (panel && !panel.dataset.loaded) {
    if (expandBtn) expandBtn.disabled = true;
    try {
      if (isTestSignalId(signalId)) {
        const testSig = localTestSignals.find(function (x) { return x.signal_id === signalId; });
        panel.innerHTML = renderTestSignalDetailPanel(testSig);
      } else {
        const res = await call("radar_tracking_detail", signalId);
        panel.innerHTML = renderSignalDetailPanel(res.data || {});
      }
      panel.dataset.loaded = "1";
    } catch (e) {
      panel.innerHTML = '<p class="hint">' + escapeHtml(String(e.message || e)) + "</p>";
    } finally {
      if (expandBtn) expandBtn.disabled = false;
    }
  }

  detailRow.hidden = false;
  detailRow.classList.add("is-open");
  if (expandBtn) expandBtn.classList.add("is-open");
  mainRow.classList.add("is-expanded");
}

function clearChartPriceLines() {
  if (!signalChartSeries || !signalChartPriceLines.length) {
    signalChartPriceLines = [];
    return;
  }
  signalChartPriceLines.forEach(function (line) {
    try {
      signalChartSeries.removePriceLine(line);
    } catch (_) {
      /* ignore */
    }
  });
  signalChartPriceLines = [];
}

function addChartPriceLine(series, price, color, title, solid) {
  if (!series || price == null || Number.isNaN(Number(price))) return;
  const line = series.createPriceLine({
    price: Number(price),
    color: color,
    lineWidth: 1,
    lineStyle:
      solid === false
        ? LightweightCharts.LineStyle.Dashed
        : LightweightCharts.LineStyle.Solid,
    axisLabelVisible: true,
    title: title + " " + fmtPrice(price),
  });
  signalChartPriceLines.push(line);
}

function addChartLevelLines(d, pf) {
  clearChartPriceLines();
  chartLivePriceLine = null;
  if (!signalChartSeries) return;

  drawReferencePriceLines(d, pf);
  if (!d.completed) {
    setLivePriceLine(d.live_price);
  }

  const z1 = d.zone1;
  if (z1) {
    const tpHit = Number(z1.tp_hit || 0);
    const allTps1 = zoneAllTpsHit(z1);
    if (tpHit < 1) {
      addChartPriceLine(signalChartSeries, z1.entry_low, "#3d5a80", "Z1↓", false);
      addChartPriceLine(signalChartSeries, z1.entry_high, "#3d5a80", "Z1↑", false);
    }
    if (allTps1) {
      /* TP3 — линию SL→БУ не рисуем, только метки TP на графике */
    } else if (tpHit >= 1) {
      addChartPriceLine(signalChartSeries, d.signal_price, "#FF5C5C", "SL→БУ", true);
    } else {
      addChartPriceLine(signalChartSeries, z1.sl, "#FF5C5C", "SL", true);
    }
    (z1.tps || []).forEach(function (tp, i) {
      if (i < tpHit) return;
      addChartPriceLine(signalChartSeries, tp, "#29E8C9", "TP" + (i + 1), true);
    });
  }

  const z2 = d.zone2;
  if (z2 && zone2ShouldShow(d)) {
    const tpHit2 = Number(z2.tp_hit || 0);
    const allTps2 = zoneAllTpsHit(z2);
    if (tpHit2 < 1) {
      addChartPriceLine(signalChartSeries, z2.entry_low, "#4a4080", "Z2↓", false);
      addChartPriceLine(signalChartSeries, z2.entry_high, "#4a4080", "Z2↑", false);
    }
    if (allTps2) {
      /* все TP Z2 */
    } else if (tpHit2 >= 1) {
      addChartPriceLine(signalChartSeries, d.signal_price, "#FF5C5C", "SL2→БУ", true);
    } else {
      addChartPriceLine(signalChartSeries, z2.sl, "#FF5C5C", "SL2", true);
    }
    (z2.tps || []).forEach(function (tp, i) {
      if (i < tpHit2) return;
      addChartPriceLine(signalChartSeries, tp, "#29E8C9", "Z2 TP" + (i + 1), true);
    });
  }
}

function priceFromClientY(clientY) {
  const chartEl = $("#signalChart");
  if (!chartEl || !signalChartSeries) return null;
  const rect = chartEl.getBoundingClientRect();
  const y = Math.max(0, Math.min(rect.height, clientY - rect.top));
  try {
    const p = signalChartSeries.coordinateToPrice(y);
    if (p == null || Number.isNaN(Number(p))) return null;
    return Number(p);
  } catch (_) {
    return null;
  }
}

function applyChartMeasureInteraction(on) {
  if (!signalChart) return;
  try {
    signalChart.applyOptions({
      handleScroll: {
        mouseWheel: !on,
        pressedMouseMove: !on,
        horzTouchDrag: !on,
        vertTouchDrag: !on,
      },
      handleScale: {
        axisPressedMouseMove: !on,
        mouseWheel: !on,
        pinch: !on,
      },
    });
  } catch (_) {
    /* ignore */
  }
}

function syncMeasureOverlay() {
  const overlay = $("#chartMeasureOverlay");
  if (overlay) overlay.hidden = !measureMode;
  applyChartMeasureInteraction(measureMode);
}

function priceToChartY(price) {
  if (!signalChartSeries) return null;
  try {
    const y = signalChartSeries.priceToCoordinate(Number(price));
    return y == null || Number.isNaN(Number(y)) ? null : Number(y);
  } catch (_) {
    return null;
  }
}

function hideChartRuler() {
  const el = $("#chartRuler");
  if (el) el.hidden = true;
  const priceTop = $("#chartRulerPriceTop");
  const priceBottom = $("#chartRulerPriceBottom");
  if (priceTop) priceTop.hidden = true;
  if (priceBottom) priceBottom.hidden = true;
}

function xFromClientX(clientX) {
  const wrap = $("#signalChartWrap");
  if (!wrap) return null;
  const rect = wrap.getBoundingClientRect();
  return Math.max(0, Math.min(rect.width, clientX - rect.left));
}

function showChartRuler(priceA, priceB, xA, xB) {
  const ruler = $("#chartRuler");
  if (!ruler) return;
  const label = ruler.querySelector(".chart-ruler-label");
  const band = ruler.querySelector(".chart-ruler-band");
  const arrow = ruler.querySelector(".chart-ruler-arrow");
  const lineTop = ruler.querySelector(".chart-ruler-line-top");
  const lineBottom = ruler.querySelector(".chart-ruler-line-bottom");

  const yA = priceToChartY(priceA);
  const yB = priceToChartY(priceB);
  if (yA == null || yB == null || xA == null || xB == null) return;

  const top = Math.min(yA, yB);
  const height = Math.max(Math.abs(yB - yA), 2);
  const left = Math.min(xA, xB);
  const width = Math.max(Math.abs(xB - xA), 20);

  ruler.style.top = top + "px";
  ruler.style.height = height + "px";
  ruler.style.left = left + "px";
  ruler.style.width = width + "px";
  ruler.hidden = false;

  if (band) {
    band.style.top = "0";
    band.style.height = "100%";
  }
  if (lineTop) lineTop.style.top = "0";
  if (lineBottom) {
    lineBottom.style.top = "auto";
    lineBottom.style.bottom = "0";
  }

  const hi = Math.max(priceA, priceB);
  const lo = Math.min(priceA, priceB);
  const delta = lo - hi;
  const pct = pctBetween(hi, lo);
  if (label) {
    label.textContent =
      fmtPrice(lo) + " → " + fmtPrice(hi) + " · " + fmtPrice(delta) + " (" + fmtPct(pct) + ")";
  }
  const priceTop = $("#chartRulerPriceTop");
  const priceBottom = $("#chartRulerPriceBottom");
  if (priceTop) {
    priceTop.textContent = fmtPrice(hi);
    priceTop.style.top = Math.max(0, Math.min(yA, yB) - 10) + "px";
    priceTop.hidden = false;
  }
  if (priceBottom) {
    priceBottom.textContent = fmtPrice(lo);
    priceBottom.style.top = Math.max(0, Math.max(yA, yB) - 10) + "px";
    priceBottom.hidden = false;
  }
  if (arrow) {
    arrow.classList.toggle("chart-ruler-arrow-up", priceB > priceA);
    arrow.classList.toggle("chart-ruler-arrow-down", priceB <= priceA);
  }

  if (signalChartDetail) {
    const sig = Number(signalChartDetail.signal_price);
    const extra =
      sig && !Number.isNaN(sig)
        ? " · от сигнала: " + fmtPct(pctBetween(sig, priceB))
        : "";
    showMeasureText("Линейка: " + fmtPrice(delta) + " (" + fmtPct(pct) + ")" + extra);
  }
}

function showMeasureText(text) {
  const out = $("#signalMeasureOut");
  if (!out) return;
  out.hidden = false;
  out.textContent = text;
}

function hideMeasureText() {
  const out = $("#signalMeasureOut");
  if (out) {
    out.hidden = true;
    out.textContent = "";
  }
}

function resetMeasureState() {
  rulerDragging = false;
  rulerStartPrice = null;
  rulerStartX = null;
  hideChartRuler();
  hideMeasureText();
}

function setupChartRulerDrag() {
  if (rulerDragBound) return;
  const overlay = $("#chartMeasureOverlay");
  if (!overlay) return;
  rulerDragBound = true;

  overlay.addEventListener("mousedown", function (e) {
    if (!measureMode || e.button !== 0) return;
    const price = priceFromClientY(e.clientY);
    const x = xFromClientX(e.clientX);
    if (price == null || x == null) return;
    rulerDragging = true;
    rulerStartPrice = price;
    rulerStartX = x;
    showChartRuler(price, price, x, x);
    e.preventDefault();
    e.stopPropagation();
  });

  overlay.addEventListener("mousemove", onRulerDragMove);

  document.addEventListener("mousemove", onRulerDragMove);

  function onRulerDragMove(e) {
    if (!rulerDragging || rulerStartPrice == null || rulerStartX == null) return;
    const price = priceFromClientY(e.clientY);
    const x = xFromClientX(e.clientX);
    if (price == null || x == null) return;
    showChartRuler(rulerStartPrice, price, rulerStartX, x);
    e.preventDefault();
  }

  overlay.addEventListener("mouseup", function () {
    rulerDragging = false;
  });

  document.addEventListener("mouseup", function () {
    rulerDragging = false;
  });
}

function setMeasureMode(on) {
  measureMode = !!on;
  const btn = $("#btnChartMeasure");
  const wrap = $("#signalChartWrap");
  if (btn) btn.classList.toggle("active", measureMode);
  if (wrap) wrap.classList.toggle("measure-mode", measureMode);
  syncMeasureOverlay();
  if (!measureMode) resetMeasureState();
  else showMeasureText("📏 Зажми точку на графике и тяни — ширина и высота как на BingX");
}

function setLivePriceLine(livePrice) {
  if (!signalChartSeries || livePrice == null || Number.isNaN(Number(livePrice))) return;
  const price = Number(livePrice);
  const title = "Сейчас " + fmtPrice(price);
  if (chartLivePriceLine) {
    chartLivePriceLine.applyOptions({ price: price, title: title });
    return;
  }
  chartLivePriceLine = signalChartSeries.createPriceLine({
    price: price,
    color: "#FFB020",
    lineWidth: 1,
    lineStyle: LightweightCharts.LineStyle.Solid,
    axisLabelVisible: true,
    title: title,
  });
  signalChartPriceLines.push(chartLivePriceLine);
}

function removeSignalMarkerSeries() {
  if (signalMarkerSeries && signalChart) {
    try {
      signalChart.removeSeries(signalMarkerSeries);
    } catch (_) {
      /* ignore */
    }
  }
  signalMarkerSeries = null;
}

function snapMarkerTimeMs(timeMs, candles) {
  if (!timeMs || !candles || !candles.length) return null;
  const sec = Math.floor(Number(timeMs) / 1000);
  return snapMarkerTime(sec, candles);
}

function addSignalEntryMarker(signalTime, signalPrice, candles, pf) {
  removeSignalMarkerSeries();
  if (!signalChart || !signalTime || signalPrice == null || Number.isNaN(Number(signalPrice))) return;

  const t = snapMarkerTime(signalTime, candles);
  const price = Number(signalPrice);
  if (!t) return;

  signalMarkerSeries = signalChart.addLineSeries({
    color: "rgba(0,0,0,0)",
    lineWidth: 0,
    crosshairMarkerVisible: false,
    lastValueVisible: false,
    priceLineVisible: false,
    pointMarkersVisible: false,
    priceFormat: {
      type: "price",
      precision: pf.precision,
      minMove: pf.minMove,
    },
  });
  signalMarkerSeries.setData([{ time: t, value: price }]);
  signalMarkerSeries.setMarkers([
    {
      time: t,
      position: "inBar",
      shape: "circle",
      color: "#F2F2F0",
      text: "S",
    },
  ]);
}

function removeTpMarkerSeries() {
  if (tpMarkerSeries && signalChart) {
    try {
      signalChart.removeSeries(tpMarkerSeries);
    } catch (_) {
      /* ignore */
    }
  }
  tpMarkerSeries = null;
}

function addTpPriceMarkers(candles, pf) {
  removeTpMarkerSeries();
  if (!signalChart || !signalChartMarkerHits.length) return;

  const tpHits = signalChartMarkerHits.filter(function (h) {
    return /TP\d+/.test(String(h.text || ""));
  });
  if (!tpHits.length) return;

  const points = [];
  const markers = [];
  tpHits.forEach(function (hit) {
    const price = hit.tpPrice;
    if (price == null || Number.isNaN(Number(price)) || !hit.time) return;
    points.push({ time: hit.time, value: Number(price) });
    markers.push({
      time: hit.time,
      position: "inBar",
      shape: "circle",
      color: hit.color || "#29E8C9",
      text: hit.text,
    });
  });
  if (!points.length) return;

  points.sort(function (a, b) {
    return a.time - b.time;
  });
  markers.sort(function (a, b) {
    if (a.time === b.time) return String(a.text).localeCompare(String(b.text));
    return a.time - b.time;
  });

  tpMarkerSeries = signalChart.addLineSeries({
    color: "rgba(0,0,0,0)",
    lineWidth: 0,
    crosshairMarkerVisible: false,
    lastValueVisible: false,
    priceLineVisible: false,
    pointMarkersVisible: false,
    priceFormat: {
      type: "price",
      precision: pf.precision,
      minMove: pf.minMove,
    },
  });
  tpMarkerSeries.setData(points);
  tpMarkerSeries.setMarkers(markers);
}

function applyChartMarkers(payload, candles, pf) {
  if (!signalChartSeries) return;

  const d = payload.detail || {};
  const markers = [];
  signalChartMarkerHits = [];
  removeTpMarkerSeries();

  (payload.markers || []).forEach(function (m) {
    const t = snapMarkerTimeMs(m.time_ms, candles);
    if (!t) return;
    const text = String(m.text || "");
    const isTp = /^(?:Z2 )?TP\d+$/.test(text);
    if (/^БУ$|^Z2 БУ$/.test(text)) {
      const z1 = d.zone1;
      if (zoneAllTpsHit(z1)) return;
    }
    const hit = {
      time: t,
      timeMs: Number(m.time_ms || 0),
      text: text,
      color: m.color || "#29E8C9",
      position: isTp ? "inBar" : m.position || "belowBar",
      tpLevel: Number(m.tp_level || 0) || null,
      tpPrice: m.tp_price != null ? Number(m.tp_price) : null,
      zoneKey: m.zone_key || "zone1",
    };
    signalChartMarkerHits.push(hit);
    if (!isTp) {
      markers.push({
        time: t,
        position: hit.position,
        shape: m.shape || "circle",
        color: hit.color,
        text: text,
      });
    }
  });

  markers.sort(function (a, b) {
    if (a.time === b.time) return String(a.text).localeCompare(String(b.text));
    return a.time - b.time;
  });
  signalChartSeries.setMarkers(markers);
  addTpPriceMarkers(candles, pf);
  if (!signalSetupClosed(d)) {
    addSignalEntryMarker(payload.signal_time, d.signal_price, candles, pf);
  }
}

function findChartMarkerHitAt(time, clickY) {
  if (!time || !signalChartMarkerHits.length) return null;
  const sec = Number(time);
  const tfSec = signalChartTf === "1m" ? 60 : signalChartTf === "5m" ? 300 : 900;
  const windowSec = tfSec * 2;
  const candidates = signalChartMarkerHits.filter(function (m) {
    return Math.abs(Number(m.time) - sec) <= windowSec;
  });
  if (!candidates.length) return null;
  if (candidates.length === 1) return candidates[0];

  if (clickY != null && tpMarkerSeries) {
    let clickPrice = null;
    try {
      clickPrice = tpMarkerSeries.coordinateToPrice(Number(clickY));
    } catch (_) {
      clickPrice = null;
    }
    if (clickPrice != null && !Number.isNaN(Number(clickPrice))) {
      let best = null;
      let bestDist = Infinity;
      candidates.forEach(function (m) {
        if (m.tpPrice == null || Number.isNaN(Number(m.tpPrice))) return;
        const dist = Math.abs(Number(m.tpPrice) - Number(clickPrice));
        if (dist < bestDist) {
          bestDist = dist;
          best = m;
        }
      });
      if (best) return best;
    }
  }

  let best = candidates[0];
  let bestDiff = Math.abs(Number(best.time) - sec);
  candidates.forEach(function (m) {
    const diff = Math.abs(Number(m.time) - sec);
    if (diff < bestDiff) {
      bestDiff = diff;
      best = m;
    } else if (diff === bestDiff && Number(m.tpLevel || 0) > Number(best.tpLevel || 0)) {
      best = m;
    }
  });
  return best;
}

function resolveMarkerHitMeta(hit) {
  const raw = signalChartLastDetail || signalChartDetail || {};
  const signalId = raw.signal_id || signalModalId || "";
  const d = applySignalReferencePrices(Object.assign({}, raw), signalId);
  const label = String(hit.text || "").trim();
  const tpMatch = label.match(/^(?:Z2 )?TP(\d+)$/);
  const isZ2 = label.indexOf("Z2") === 0;
  const zone = isZ2 && d.zone2 ? d.zone2 : d.zone1;
  let tpLevel = hit.tpLevel;
  let tpPrice = hit.tpPrice;
  if (tpMatch) {
    tpLevel = Number(tpMatch[1]);
    const idx = tpLevel - 1;
    if (zone && zone.tps && zone.tps[idx] != null) {
      tpPrice = Number(zone.tps[idx]);
    }
  }
  const when = hit.timeMs ? formatKyivChartTime(Math.floor(hit.timeMs / 1000)) : "—";
  const sym = displaySymbolFromDetail(d, signalId);
  const sig = Number(d.signal_price);
  const send = Number(d.send_price);
  let pnlSig = null;
  let pnlSend = null;
  if (tpPrice != null && !Number.isNaN(sig) && sig > 0) {
    pnlSig = ((sig - tpPrice) / sig) * 100;
  }
  if (tpPrice != null && !Number.isNaN(send) && send > 0) {
    pnlSend = ((send - tpPrice) / send) * 100;
  }
  return {
    label: label,
    tpLevel: tpLevel,
    tpPrice: tpPrice,
    when: when,
    symbol: sym,
    signalId: signalId || "—",
    zoneLabel: isZ2 ? "Зона 2" : "Зона 1",
    signalPrice: sig,
    sendPrice: send,
    pnlSignalPct: pnlSig,
    pnlSendPct: pnlSend,
  };
}

function hideTpHitPopover() {
  const pop = $("#tpHitPopover");
  if (pop) pop.hidden = true;
}

function showTpHitPopover(hit, param) {
  const meta = resolveMarkerHitMeta(hit);
  const pop = $("#tpHitPopover");
  const title = $("#tpHitPopoverTitle");
  const sub = $("#tpHitPopoverSub");
  const detailsBtn = $("#tpHitPopoverDetails");
  if (!pop || !title || !sub || !detailsBtn) return;

  title.textContent = (meta.tpLevel ? "TP" + meta.tpLevel + " взят" : meta.label) + " · " + meta.symbol;
  sub.textContent =
    (meta.tpPrice != null ? "Цель " + fmtPrice(meta.tpPrice) : meta.label) +
    " · " +
    meta.when +
    " (Киев)";

  detailsBtn.onclick = function () {
    hideTpHitPopover();
    openTpHitModal(hit);
  };

  const wrap = $("#signalChartWrap");
  const x = param && param.point ? param.point.x : 0;
  const y = param && param.point ? param.point.y : 0;
  if (wrap) {
    const maxX = Math.max(24, wrap.clientWidth - 24);
    const maxY = Math.max(24, wrap.clientHeight - 24);
    pop.style.left = Math.min(Math.max(24, x), maxX) + "px";
    pop.style.top = Math.min(Math.max(24, y), maxY) + "px";
  }
  pop.hidden = false;
}

function closeTpHitModal() {
  const modal = $("#tpHitModal");
  if (modal) modal.hidden = true;
}

function openTpHitModal(hit) {
  const meta = resolveMarkerHitMeta(hit);
  const modal = $("#tpHitModal");
  const body = $("#tpHitModalBody");
  const title = $("#tpHitModalTitle");
  if (!modal || !body || !title) return;

  title.textContent = "Детали · " + (meta.tpLevel ? "TP" + meta.tpLevel : meta.label);
  const rows = [
    ["Сигнал", meta.signalId],
    ["Тикер", meta.symbol],
    ["Направление", "SHORT"],
    ["Зона", meta.zoneLabel],
    ["Событие", meta.tpLevel ? "TP" + meta.tpLevel + " взят" : meta.label],
    ["Время (Киев)", meta.when],
    ["Цель TP", meta.tpPrice != null ? fmtPrice(meta.tpPrice) : "—"],
    ["Сигнальная цена", fmtPrice(meta.signalPrice)],
    ["Цена отправки", fmtPrice(meta.sendPrice)],
    [
      "% от сигнальной",
      meta.pnlSignalPct != null ? (meta.pnlSignalPct >= 0 ? "+" : "") + meta.pnlSignalPct.toFixed(2) + "%" : "—",
    ],
    [
      "% от отправки",
      meta.pnlSendPct != null ? (meta.pnlSendPct >= 0 ? "+" : "") + meta.pnlSendPct.toFixed(2) + "%" : "—",
    ],
  ];
  body.innerHTML = rows
    .map(function (r) {
      return "<tr><td>" + escapeHtml(r[0]) + "</td><td>" + escapeHtml(r[1]) + "</td></tr>";
    })
    .join("");
  modal.hidden = false;
}

function setupChartMarkerClick() {
  if (!signalChart || signalChartClickBound) return;
  signalChartClickBound = true;
  signalChart.subscribeClick(function (param) {
    if (!param || param.time == null || measureMode) return;
    const clickY = param.point ? param.point.y : null;
    const hit = findChartMarkerHitAt(param.time, clickY);
    if (!hit) {
      hideTpHitPopover();
      return;
    }
    showTpHitPopover(hit, param);
  });
}

function destroySignalChart() {
  clearChartPriceLines();
  chartLivePriceLine = null;
  removeSignalMarkerSeries();
  removeTpMarkerSeries();
  signalChartMarkerHits = [];
  signalChartClickBound = false;
  signalChartLastDetail = null;
  rulerDragging = false;
  rulerStartPrice = null;
  signalChartCandles = [];
  signalChartDetail = null;
  if (signalChart) {
    signalChart.remove();
    signalChart = null;
    signalChartSeries = null;
  }
}

function stopSignalLivePolling() {
  if (signalLivePoll) {
    clearInterval(signalLivePoll);
    signalLivePoll = null;
  }
  if (signalCandlePoll) {
    clearInterval(signalCandlePoll);
    signalCandlePoll = null;
  }
}

function startSignalLivePolling() {
  stopSignalLivePolling();
  signalLivePoll = setInterval(tickSignalLive, 1000);
  signalCandlePoll = setInterval(async function () {
    if (!signalModalId) return;
    try {
      await loadSignalChart(signalModalId, signalChartTf, true);
    } catch (_) {
      /* ignore */
    }
  }, 30000);
}

async function tickSignalLive() {
  if (!signalModalId || !signalChartSeries || !signalChartDetail) return;
  try {
    const res = await call("radar_tracking_live", signalModalId);
    const live = res.data && res.data.live_price;
    if (live == null || Number.isNaN(Number(live))) return;
    applyLivePrice(Number(live));
  } catch (_) {
    /* ignore */
  }
}

function applyLivePrice(livePrice) {
  if (!signalChartDetail) return;
  signalChartDetail.live_price = livePrice;
  const sp = Number(signalChartDetail.signal_price);
  const send = Number(signalChartDetail.send_price);
  if (sp) {
    signalChartDetail.deviation_signal_pct = Math.round(((livePrice - sp) / sp) * 10000) / 100;
  }
  if (send) {
    signalChartDetail.deviation_send_pct = Math.round(((livePrice - send) / send) * 10000) / 100;
  }
  if (signalChartLastDetail) {
    signalChartLastDetail.live_price = signalChartDetail.live_price;
    signalChartLastDetail.deviation_signal_pct = signalChartDetail.deviation_signal_pct;
    signalChartLastDetail.deviation_send_pct = signalChartDetail.deviation_send_pct;
  }
  $("#signalStats").innerHTML = renderSignalStats(signalChartDetail);
  setLivePriceLine(livePrice);

  if (!signalChartCandles.length) return;
  const last = signalChartCandles[signalChartCandles.length - 1];
  const updated = {
    time: last.time,
    open: last.open,
    high: Math.max(last.high, livePrice),
    low: Math.min(last.low, livePrice),
    close: livePrice,
  };
  signalChartCandles[signalChartCandles.length - 1] = updated;
  signalChartSeries.update(updated);
}

function sortedCandles(payload) {
  return (payload.candles || []).slice().sort(function (a, b) {
    return a.time - b.time;
  });
}

function updateChartMeta(payload, candles) {
  const meta = $("#signalTfMeta");
  if (!meta) return;
  const tf = payload.timeframe || signalChartTf;
  const count = payload.candle_count || candles.length;
  if (candles.length >= 2) {
    meta.textContent =
      tf +
      " · " +
      count +
      " св. · " +
      formatKyivChartTime(candles[0].time) +
      " – " +
      formatKyivChartTime(candles[candles.length - 1].time) +
      " (Киев) · live";
  } else {
    meta.textContent = tf + " · свечей: " + count + " · live";
  }
}

function applyChartPayload(payload, softUpdate) {
  const d0 = payload.detail || {};
  const d = applySignalReferencePrices(Object.assign({}, d0), d0.signal_id || signalModalId);
  payload = Object.assign({}, payload, { detail: d });
  const candles = sortedCandles(payload);
  const pf = chartPriceFormat(collectChartPrices(d, candles));
  signalChartDetail = d;
  signalChartLastDetail = d;
  signalChartCandles = candles.slice();
  signalChartSignalTime = payload.signal_time || 0;

  if (softUpdate && signalChart && signalChartSeries) {
    if (candles.length) {
      signalChartSeries.setData(candles);
    }
    addChartLevelLines(d, pf);
    applyChartMarkers(payload, candles, pf);
    $("#signalStats").innerHTML = renderSignalStats(d);
    updateChartMeta(payload, candles);
    return;
  }

  renderSignalChart(payload);
}

function renderSignalChart(payload) {
  const container = $("#signalChart");
  const d = payload.detail || {};
  if (!container || typeof LightweightCharts === "undefined") return;

  destroySignalChart();
  const w = container.clientWidth || 860;
  const candles = sortedCandles(payload);
  const pf = chartPriceFormat(collectChartPrices(d, candles));

  signalChart = LightweightCharts.createChart(container, {
    layout: { background: { color: "#0A0A0A" }, textColor: "#8F8F8F" },
    grid: { vertLines: { color: "#1a1a1a" }, horzLines: { color: "#1a1a1a" } },
    crosshair: { mode: LightweightCharts.CrosshairMode.Normal },
    rightPriceScale: {
      borderColor: "#333333",
      autoScale: true,
    },
    timeScale: { borderColor: "#333333", timeVisible: true, secondsVisible: false },
    localization: {
      locale: "ru-UA",
      dateFormat: "dd.MM",
      priceFormatter: function (p) {
        return fmtPrice(p);
      },
      timeFormatter: formatKyivChartTime,
    },
    width: w,
    height: 420,
  });

  signalChartSeries = signalChart.addCandlestickSeries({
    upColor: "#29E8C9",
    downColor: "#FF5C5C",
    borderVisible: false,
    wickUpColor: "#29E8C9",
    wickDownColor: "#FF5C5C",
    priceFormat: {
      type: "price",
      precision: pf.precision,
      minMove: pf.minMove,
    },
  });

  if (candles.length) {
    signalChartSeries.setData(candles);
  }

  addChartLevelLines(d, pf);
  applyChartMarkers(payload, candles, pf);
  setupChartMarkerClick();
  signalChart.timeScale().fitContent();
  updateChartMeta(payload, candles);
  syncMeasureOverlay();
}

function renderSignalModal(payload, softChart) {
  const d0 = payload.detail || {};
  const d = applySignalReferencePrices(Object.assign({}, d0), d0.signal_id || signalModalId);
  payload = Object.assign({}, payload, { detail: d });
  signalModalBingxUrl = d.bingx_url || "";
  if (!softChart) {
    $("#signalModalTitle").textContent = d.display_symbol || d.symbol || "—";
    const sub = d.signal_id || "";
    const result = d.result_label || (d.completed ? d.outcome : "");
    $("#signalModalSub").textContent = result ? sub + " · " + result : sub;
    let levelsHtml = renderZoneCard(d.zone1, d.signal_price);
    if (zone2ShouldShow(d)) {
      levelsHtml += renderZoneCard(d.zone2, d.signal_price);
    } else if (d.zone2 && d.zone2_disabled && !d.zone2.activated) {
      levelsHtml +=
        '<div class="signal-zone-card"><div class="signal-zone-title">Зона 2</div><div class="signal-zone-row"><span>Статус</span><span>отключена (Z1 TP)</span></div></div>';
    }
    $("#signalLevels").innerHTML = levelsHtml;
  }
  $("#signalStats").innerHTML = renderSignalStats(d);
  applyChartPayload(payload, !!softChart);
}

async function loadSignalChart(signalId, tf, softUpdate) {
  if (tf) setSignalTfActive(tf);
  const res = await call("radar_tracking_chart", signalId, signalChartTf);
  renderSignalModal(res.data || {}, !!softUpdate);
}

async function openSignalModal(signalId) {
  if (!canViewSignalInsights(signalId)) {
    showToast(signalsBlockedMessage(), "error");
    return;
  }
  signalModalId = signalId;
  signalChartTf = "1m";
  const modal = $("#signalModal");
  if (modal) modal.hidden = false;
  $("#signalModalTitle").textContent = "…";
  $("#signalModalSub").textContent = signalId;
  $("#signalStats").innerHTML = "";
  $("#signalLevels").innerHTML = "";
  destroySignalChart();

  try {
    await loadSignalChart(signalId, "1m");
  } catch (e) {
    const msg = String(e.message || e);
    if (/404|not found/i.test(msg)) {
      showToast("График не найден. Обнови bot на сервере (admin_api, signal_tracking, database) и перезапусти.", "error");
    } else {
      showToast(msg, "error");
    }
    closeSignalModal();
    return;
  }

  if (signalChartDetail && signalChartDetail.completed) {
    stopSignalLivePolling();
  } else {
    startSignalLivePolling();
  }
}

function closeSignalModal() {
  signalModalId = null;
  signalModalBingxUrl = "";
  setMeasureMode(false);
  hideTpHitPopover();
  closeTpHitModal();
  stopSignalLivePolling();
  destroySignalChart();
  const modal = $("#signalModal");
  if (modal) modal.hidden = true;
}

function ensureTrackingShell() {
  const wrap = $("#trackingWrap");
  if (!wrap) return null;
  let empty = $("#trackingEmpty");
  if (!empty) {
    wrap.insertAdjacentHTML(
      "beforeend",
      '<div class="tracking-empty" id="trackingEmpty" hidden>' +
        "<strong>Нет активных сопровождений</strong>" +
        "<span>После сигнала бот ведёт токен до TP, SL или БУ. Состояние сохраняется после перезапуска.</span>" +
        "</div>"
    );
    empty = $("#trackingEmpty");
  }
  return { wrap: wrap, empty: empty };
}

function normalizeTrackingItems(raw) {
  if (Array.isArray(raw)) return raw;
  if (raw && Array.isArray(raw.items)) return raw.items;
  return [];
}

function clearTrackingRows() {
  const shell = ensureTrackingShell();
  if (!shell) return;
  shell.wrap.querySelectorAll(".tracking-row").forEach(function (row) {
    row.remove();
  });
}

function formatTrackingResult(t) {
  if (t.result_label) return t.result_label;
  const tp = Number(t.tp_hit || 0);
  if (t.sl_hit) return "стоп";
  if (t.bu_hit) return tp >= 1 ? tp + " ТП + БУ" : "БУ";
  if (tp > 0) return tp + " ТП";
  return "ожидание";
}

async function renderTracking() {
  const shell = ensureTrackingShell();
  if (!shell) return;
  const wrap = shell.wrap;
  const empty = shell.empty;
  const card = $("#trackingCard");

  let apiItems = [];
  try {
    const res = await call("radar_tracking");
    apiItems = normalizeTrackingItems(res.data);
  } catch (e) {
    if (!localTestTracking) {
      showToast(String(e.message || e), "error");
      return;
    }
  }

  try {
    const merged = mergeTrackingItems(apiItems);
    const items = trackingItemsForUser(merged);
    if (!items.length) {
      wrap.querySelectorAll(".tracking-row").forEach(function (row) { row.remove(); });
      empty.hidden = false;
      if (card) card.classList.add("is-empty");
      return;
    }
    empty.hidden = true;
    if (card) card.classList.remove("is-empty");
    const rowsHtml = items
      .map(function (t) {
        const sym = String(t.symbol || "?").replace("/USDT", "");
        const statusParts = [formatTrackingResult(t)];
        const isTest = !!(t.is_test || isTestTrackingId(t.signal_id));
        const checked = t.tracking_enabled !== false ? " checked" : "";
        const canToggle = isRealAdmin();
        const toggleHtml = canToggle
          ? ('<label class="toggle tracking-toggle">' +
            '<input type="checkbox" class="track-toggle"' + checked + ">" +
            '<span class="toggle-track"><span class="toggle-thumb"></span></span>' +
            "</label>" +
            '<span class="tracking-toggle-label">' + (t.tracking_enabled !== false ? "Ведём" : "Выкл") + "</span>")
          : ('<span class="tracking-status-pill">' + (t.tracking_enabled !== false ? "Сопровождение" : "—") + "</span>");
        return (
          '<div class="tracking-row' + (isTest ? " tracking-test-row" : "") + '" data-signal-id="' +
          String(t.signal_id).replace(/"/g, "&quot;") +
          '" data-test="' + (isTest ? "1" : "0") + '">' +
          '<div class="tracking-main">' +
          '<div class="tracking-symbol">' + sym + (isTest ? ' <span class="pill pill-test pill-test-inline">тест</span>' : "") + "</div>" +
          '<span class="tracking-id">' + (t.signal_id || "—") + (isTest ? " · демо" : " · нажми для графика") + "</span>" +
          "</div>" +
          '<div class="tracking-meta">' + statusParts.join(" · ") + "</div>" +
          toggleHtml +
          "</div>"
        );
      })
      .join("");
    wrap.querySelectorAll(".tracking-row").forEach(function (row) { row.remove(); });
    empty.insertAdjacentHTML("beforebegin", rowsHtml);
    $all(".tracking-row", wrap).forEach(function (row) {
      row.addEventListener("click", function (e) {
        if (e.target.closest(".tracking-toggle") || e.target.closest(".track-toggle")) return;
        const signalId = row.dataset.signalId;
        if (row.dataset.test === "1") {
          showToast("Тест сопровождения — график недоступен");
          return;
        }
        if (signalId) openSignalModal(signalId);
      });
    });
    if (isRealAdmin()) {
      $all(".track-toggle", wrap).forEach(function (inp) {
        inp.addEventListener("click", function (e) {
          e.stopPropagation();
        });
        inp.addEventListener("change", async function (e) {
          const row = inp.closest(".tracking-row");
          const signalId = row ? row.dataset.signalId : "";
          if (!signalId) return;
          inp.disabled = true;
          try {
            if (isTestTrackingId(signalId) && localTestTracking) {
              localTestTracking.tracking_enabled = e.target.checked;
              const label = row.querySelector(".tracking-toggle-label");
              if (label) label.textContent = e.target.checked ? "Ведём" : "Выкл";
              showToast(
                e.target.checked
                  ? "Тест: сопровождение включено (участники видят)"
                  : "Тест: сопровождение выключено (участникам скрыто)"
              );
              renderTracking();
              return;
            }
            await call("set_tracking", signalId, e.target.checked);
            const label = row.querySelector(".tracking-toggle-label");
            if (label) label.textContent = e.target.checked ? "Ведём" : "Выкл";
            showToast(e.target.checked ? "Сопровождение включено: " + signalId : "Сопровождение выключено: " + signalId);
          } catch (err) {
            showToast(String(err.message || err), "error");
            e.target.checked = !e.target.checked;
          } finally {
            inp.disabled = false;
          }
        });
      });
    }
  } catch (e) {
    showToast(String(e.message || e), "error");
  }
}

async function renderSignals() {
  const uiUser = getUiUser();
  const hasAccess = hasLiveSignalAccess();
  const hasTestRows = localTestSignals.length > 0;
  const outcomesOnly = !hasAccess && !hasTestRows;
  signalsOutcomesView = outcomesOnly;

  const outcomesBanner = $("#signalsOutcomesBanner");
  const hintToggle = $("#signalsHintToggle");
  const trackingCard = $("#trackingCard");
  if (outcomesBanner) outcomesBanner.hidden = !outcomesOnly;
  if (hintToggle) {
    hintToggle.hidden = false;
    hintToggle.textContent = outcomesOnly
      ? "Тикер — график отработки · стрелка — вход, TP, SL (только завершённые сигналы)."
      : "Тикер — график · стрелка справа — полные параметры сигнала (зоны, TP, SL).";
  }
  if (trackingCard) trackingCard.hidden = outcomesOnly;

  if (hasAccess) {
    await renderTracking();
  } else {
    clearTrackingRows();
    const trackEmpty = $("#trackingEmpty");
    if (trackEmpty) trackEmpty.hidden = true;
  }

  const period = $("#signalsPeriod").value;
  const hours = PERIOD_HOURS[period] || 168;
  try {
    let apiRows = [];
    if (hasAccess) {
      const res = await call("radar_signals", hours, 200);
      apiRows = res.data || [];
    } else if (!hasTestRows) {
      const res = await call("radar_signals_outcomes", hours, 200);
      apiRows = res.data || [];
    }
    const rows = mergeSignalRows(apiRows);
    const body = $("#signalsBody");
    if (!rows.length) {
      body.innerHTML = "";
      setSignalsEmptyMessage(
        outcomesOnly ? signalsOutcomesEmptyMessage() : signalsWaitingMessage()
      );
      $(".table-card table").style.display = "none";
      return;
    }
    $("#signalsEmpty").style.display = "none";
    $(".table-card table").style.display = "table";
    body.innerHTML = rows
      .map(function (s) {
        const sym = String(s.symbol || "?").replace("/USDT", "");
        const pct = s.change_24h != null ? Number(s.change_24h) : null;
        const pctText = pct != null ? "+" + pct.toFixed(1) + "%" : "—";
        const vol = s.volume_24h ? "$" + (s.volume_24h / 1e6).toFixed(1) + "M" : "—";
        const sidRaw = s.signal_id || signalIdFromRow(s.symbol, s.timestamp);
        const sid = String(sidRaw || "").replace(/"/g, "&quot;");
        const symSafe = sym.replace(/"/g, "&quot;");
        const result = s.result || "—";
        const isTest = !!(s.is_test || isTestSignalId(sidRaw));
        if (!sidRaw) return "";
        const actionsCell =
          '<td class="signal-expand-cell">' +
          (!outcomesOnly && isTest
            ? '<button type="button" class="signal-delete-test-btn" title="Удалить тест" aria-label="Удалить тест">×</button>'
            : "") +
          '<button type="button" class="signal-expand-btn" title="Параметры сигнала" aria-label="Параметры сигнала">' +
          '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>' +
          "</button></td>";
        const detailRow =
          '<tr class="signal-detail-row" data-signal-id="' +
          sid +
          '" hidden><td colspan="7">' +
          '<div class="signal-detail-panel"><p class="hint">Загрузка…</p></div>' +
          "</td></tr>";
        return (
          '<tr class="signal-history-row' +
          (isTest ? " signal-test-row" : "") +
          (outcomesOnly ? " signal-outcome-row" : "") +
          '" data-signal-id="' +
          sid +
          '" data-symbol="' +
          symSafe +
          '" data-ts="' +
          String(s.timestamp || "") +
          '">' +
          '<td class="mono">' +
          formatKyiv(s.timestamp) +
          "</td>" +
          '<td class="mono signal-ticker-cell" title="Открыть график">' +
          sym +
          (isTest ? ' <span class="pill pill-test pill-test-inline">тест</span>' : "") +
          "</td>" +
          '<td><span class="pill ' + pillClass(s.pump_type) + '">' + (s.pump_type || "—") + "</span></td>" +
          '<td class="mono signal-result">' + result + "</td>" +
          '<td class="mono ' + (pct != null ? pctClass(pct) : "") + '">' + pctText + "</td>" +
          '<td class="mono">' + vol + "</td>" +
          actionsCell +
          "</tr>" +
          detailRow
        );
      })
      .join("");
    $all(".signal-history-row", body).forEach(function (row) {
      const sid = resolveSignalRowId(row);
      if (!sid) return;
      const tickerCell = row.querySelector(".signal-ticker-cell");
      if (tickerCell) {
        tickerCell.addEventListener("click", function (e) {
          e.stopPropagation();
          if (isTestSignalId(sid)) {
            showToast("Тестовый сигнал — график недоступен");
            void toggleSignalRowDetail(sid, row, expandBtn);
            return;
          }
          openSignalModal(sid);
        });
      }
      const expandBtn = row.querySelector(".signal-expand-btn");
      const deleteBtn = row.querySelector(".signal-delete-test-btn");
      if (deleteBtn) {
        deleteBtn.addEventListener("click", function (e) {
          e.stopPropagation();
          removeTestSignal(sid);
        });
      }
      if (expandBtn) {
        expandBtn.addEventListener("click", function (e) {
          e.stopPropagation();
          void toggleSignalRowDetail(sid, row, expandBtn);
        });
      }
    });
  } catch (e) {
    showToast(String(e.message || e), "error");
  }
}

async function loadBanListPayload() {
  let cfg = { data: {} };
  try {
    cfg = await call("load_config");
  } catch (_) {}
  const hasAdminToken = !!(cfg.data && String(cfg.data.token || "").trim());
  if (isAdminUi() && hasAdminToken) {
    try {
      return await call("ban_list");
    } catch (_) {
      /* fallback: exe without ADMIN_TOKEN */
    }
  }
  return await call("radar_ban_list");
}

async function renderBanList() {
  try {
    if (!connected) {
      const wrap = $("#banListWrap");
      if (wrap) {
        wrap.innerHTML = '<div class="empty-state" style="padding:24px;"><span>Подключись к боту во вкладке «Подключение».</span></div>';
      }
      return;
    }
    const res = await loadBanListPayload();
    const payload = res.data || {};
    const readOnly = !!payload.read_only || !isAdminUi();
    const ordered = Array.isArray(payload.ordered)
      ? payload.ordered
      : []
        .concat(Array.isArray(payload.items) ? payload.items.map(function (b) {
          return { symbol: b.symbol, reason: b.reason || "—", source: "user" };
        }) : [])
        .concat(Array.isArray(payload.builtin) ? payload.builtin : []);
    const wrap = $("#banListWrap");
    if (!wrap) return;

    function rowHtml(b, index, list) {
      let section = "";
      if (index > 0 && b.source === "builtin" && list[index - 1].source === "user") {
        section = '<div class="ban-section-label">Стандартные исключения</div>';
      }
      const removeBtn = readOnly
        ? ""
        : '<button class="icon-btn" data-symbol="' + b.symbol + '" title="Удалить">' +
          '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>' +
          "</button>";
      const sourceClass = b.source === "builtin" ? " ban-row-builtin" : " ban-row-user";
      const reason = b.source === "builtin" ? "" : (b.reason && b.reason !== "—" ? b.reason : "");
      const reasonHtml = reason
        ? '<span class="ban-reason">' + escapeHtml(reason) + "</span>"
        : "";
      return (
        section +
        '<div class="ban-row' + sourceClass + '">' +
        '<div class="ban-main"><span class="ban-ticker">' + b.symbol + "</span>" + reasonHtml + "</div>" +
        removeBtn +
        "</div>"
      );
    }

    if (!ordered.length) {
      wrap.innerHTML = '<div class="empty-state" style="padding:24px;"><span>Бан-лист пуст.</span></div>';
      return;
    }

    wrap.innerHTML = ordered.map(function (b, i) { return rowHtml(b, i, ordered); }).join("");
    if (!readOnly) {
      $all("[data-symbol]", wrap).forEach(function (btn) {
        btn.addEventListener("click", async function () {
          try {
            await call("remove_ban", btn.dataset.symbol);
            showToast("Удалено: " + btn.dataset.symbol);
            renderBanList();
          } catch (e) {
            showToast(String(e.message || e), "error");
          }
        });
      });
    }
  } catch (e) {
    showToast(String(e.message || e), "error");
  }
}

async function loadSettings() {
  try {
    const res = await call("get_settings");
    $("#greetingText").value = res.data.startup_message || "";
    const groupToggle = $("#toggleStartupGroup");
    if (groupToggle) groupToggle.checked = res.data.startup_group_enabled !== false;
    renderPreview();
  } catch (e) {
    showToast(String(e.message || e), "error");
  }
}

async function renderLogs() {
  try {
    const res = await call("logs", 250);
    $("#logsContent").textContent = res.data || "Лог пуст.";
  } catch (e) {
    showToast(String(e.message || e), "error");
  }
}

let deployQueue = [];
let deployAllowed = [];

function formatDeployBytes(n) {
  const b = Number(n) || 0;
  if (b < 1024) return b + " B";
  return (b / 1024).toFixed(1) + " KB";
}

function renderDeployAllowed() {
  const el = $("#deployAllowedList");
  if (!el) return;
  if (!deployAllowed.length) {
    el.textContent = "—";
    return;
  }
  el.innerHTML = deployAllowed.map(function (name) {
    return '<span class="deploy-tag">' + escapeHtml(name) + "</span>";
  }).join("");
}

function renderDeployQueue() {
  const wrap = $("#deployQueue");
  if (!wrap) return;
  if (!deployQueue.length) {
    wrap.innerHTML = '<div class="deploy-empty">Файлы не выбраны</div>';
    return;
  }
  wrap.innerHTML = deployQueue.map(function (f, idx) {
    return (
      '<div class="deploy-queue-item">' +
      '<span class="deploy-queue-name">' + escapeHtml(f.path) + "</span>" +
      '<span class="deploy-queue-size">' + formatDeployBytes(f.content.length) + "</span>" +
      '<button type="button" class="icon-btn deploy-queue-remove" data-idx="' + idx + '" aria-label="Убрать">✕</button>' +
      "</div>"
    );
  }).join("");
  wrap.querySelectorAll(".deploy-queue-remove").forEach(function (btn) {
    btn.addEventListener("click", function () {
      deployQueue.splice(parseInt(btn.dataset.idx, 10), 1);
      renderDeployQueue();
    });
  });
}

function addToDeployQueue(rawName, content) {
  let name = String(rawName || "").trim().replace(/\\/g, "/");
  name = name.split("/").pop() || "";
  if (!name) {
    showToast("Некорректное имя файла", "error");
    return false;
  }
  if (!name.endsWith(".py")) {
    showToast("Только .py файлы", "error");
    return false;
  }
  if (deployAllowed.length && deployAllowed.indexOf(name) === -1) {
    showToast(name + " — не в whitelist сервера", "error");
    return false;
  }
  if (typeof content !== "string" || !content.length) {
    showToast("Пустой файл: " + name, "error");
    return false;
  }
  const existing = deployQueue.findIndex(function (f) { return f.path === name; });
  const item = { path: name, content: content };
  if (existing >= 0) deployQueue[existing] = item;
  else deployQueue.push(item);
  renderDeployQueue();
  return true;
}

function readDeployFiles(fileList) {
  const files = Array.from(fileList || []);
  if (!files.length) return;
  files.forEach(function (file) {
    const reader = new FileReader();
    reader.onload = function () {
      addToDeployQueue(file.name, String(reader.result || ""));
    };
    reader.onerror = function () {
      showToast("Не удалось прочитать: " + file.name, "error");
    };
    reader.readAsText(file, "utf-8");
  });
}

function renderDeployResults(data) {
  const el = $("#deployResults");
  if (!el) return;
  if (!data) {
    el.textContent = "—";
    return;
  }
  const lines = [];
  if (data.backup_dir) lines.push("Бэкап: " + data.backup_dir);
  (data.results || []).forEach(function (r) {
    if (r.ok) lines.push("✓ " + r.path + " (" + (r.bytes || 0) + " bytes)");
    else lines.push("✗ " + (r.path || "?") + " — " + (r.error || "ошибка"));
  });
  el.textContent = lines.length ? lines.join("\n") : "—";
}

async function loadDeployManifest() {
  try {
    const res = await call("deploy_manifest");
    deployAllowed = (res.data && res.data.files) || [];
    renderDeployAllowed();
  } catch (e) {
    deployAllowed = [];
    const el = $("#deployAllowedList");
    if (el) {
      el.textContent = "";
      el.innerHTML = '<span class="deploy-warn">Недоступно — один раз залей admin_api.py и deploy_files.py вручную</span>';
    }
  }
}

async function uploadDeployQueue(restartAfter) {
  if (!deployQueue.length) {
    showToast("Добавь файлы в очередь", "error");
    return;
  }
  const btn = restartAfter ? $("#btnDeployRestart") : $("#btnDeployUpload");
  const original = btn.innerHTML;
  btn.disabled = true;
  btn.textContent = restartAfter ? "Загрузка и перезапуск…" : "Загрузка…";
  try {
    const payload = deployQueue.map(function (f) {
      return { path: f.path, content: f.content };
    });
    const res = await call("deploy_files", payload);
    renderDeployResults(res.data);
    const written = (res.data && res.data.written) || 0;
    if (written > 0) {
      showToast("Обновлено файлов: " + written);
      deployQueue = deployQueue.filter(function (f) {
        const row = (res.data.results || []).find(function (r) { return r.path === f.path; });
        return !(row && row.ok);
      });
      renderDeployQueue();
    } else {
      showToast("Ни один файл не записан", "error");
    }
    if (restartAfter && written > 0) {
      await call("restart");
      showToast("Бот перезапускается");
      setTimeout(refreshOverview, 4000);
    }
  } catch (e) {
    showToast(String(e.message || e), "error");
  } finally {
    btn.disabled = false;
    btn.innerHTML = original;
  }
}

function setupDeployHandlers() {
  const pickBtn = $("#btnDeployPick");
  const fileInput = $("#deployFileInput");
  const dropZone = $("#deployDropZone");
  if (pickBtn && fileInput) {
    pickBtn.addEventListener("click", function () { fileInput.click(); });
    fileInput.addEventListener("change", function () {
      readDeployFiles(fileInput.files);
      fileInput.value = "";
    });
  }
  if (dropZone) {
    ["dragenter", "dragover"].forEach(function (ev) {
      dropZone.addEventListener(ev, function (e) {
        e.preventDefault();
        dropZone.classList.add("deploy-drop-active");
      });
    });
    ["dragleave", "drop"].forEach(function (ev) {
      dropZone.addEventListener(ev, function (e) {
        e.preventDefault();
        dropZone.classList.remove("deploy-drop-active");
      });
    });
    dropZone.addEventListener("drop", function (e) {
      readDeployFiles(e.dataTransfer && e.dataTransfer.files);
    });
  }
  const pasteBtn = $("#btnDeployPasteAdd");
  if (pasteBtn) {
    pasteBtn.addEventListener("click", function () {
      const name = $("#deployPasteName").value;
      const content = $("#deployPasteContent").value;
      if (addToDeployQueue(name, content)) {
        $("#deployPasteContent").value = "";
        showToast("Добавлено в очередь");
      }
    });
  }
  const clearBtn = $("#btnDeployClear");
  if (clearBtn) {
    clearBtn.addEventListener("click", function () {
      deployQueue = [];
      renderDeployQueue();
      showToast("Очередь очищена");
    });
  }
  const uploadBtn = $("#btnDeployUpload");
  if (uploadBtn) uploadBtn.addEventListener("click", function () { uploadDeployQueue(false); });
  const restartBtn = $("#btnDeployRestart");
  if (restartBtn) restartBtn.addEventListener("click", function () { uploadDeployQueue(true); });
}

let panelUpdateInfo = null;
let panelVersionLabel = "—";
let panelUpdateBusy = false;

const PANEL_UPDATE_AV_HINT =
  "Если Windows Defender блокирует: «Безопасность Windows» → «Журнал защиты» → восстановить файл → «Исключения» → добавить папку AXON-Dump-Radar в AppData (Roaming).";

async function loadPanelVersionInfo() {
  try {
    const res = await call("panel_version_info");
    panelVersionLabel = String(res.data.version || "—");
    const sidebar = $("#sidebarVersion");
    if (sidebar) sidebar.textContent = "v" + panelVersionLabel;
    const about = $("#aboutCurrentVersion");
    if (about) about.textContent = "v" + panelVersionLabel;
  } catch (_) {
    /* ignore */
  }
}

function renderAboutUpdateUi(info) {
  panelUpdateInfo = info || null;
  const statusEl = $("#aboutUpdateStatus");
  const notesEl = $("#aboutUpdateNotes");
  const installBtn = $("#btnInstallPanelUpdate");
  if (!statusEl) return;

  if (!info) {
    statusEl.textContent = "—";
    if (notesEl) notesEl.textContent = "";
    if (installBtn) installBtn.hidden = true;
    return;
  }

  if (info.error && !info.update_available) {
    statusEl.textContent = "Не удалось проверить";
    if (notesEl) notesEl.textContent = info.error;
    if (installBtn) installBtn.hidden = true;
    return;
  }

  if (info.update_available) {
    statusEl.textContent = "Доступна v" + (info.latest_version || "—");
    if (notesEl) {
      const base = info.notes || "Нажми «Обновить сейчас» — панель перезапустится автоматически.";
      notesEl.textContent = base + " " + PANEL_UPDATE_AV_HINT;
    }
    if (installBtn) installBtn.hidden = !info.updatable;
  } else {
    statusEl.textContent = "Актуальная версия";
    if (notesEl) notesEl.textContent = info.latest_version ? "На сервере: v" + info.latest_version : "";
    if (installBtn) installBtn.hidden = true;
  }
}

function renderUpdateModal(info) {
  const stats = $("#updateModalStats");
  if (stats) {
    stats.innerHTML =
      '<div class="signal-stat"><div class="signal-stat-label">Сейчас</div><div class="signal-stat-value">v' +
      escapeHtml(info.current_version || "—") +
      '</div></div><div class="signal-stat"><div class="signal-stat-label">Новая</div><div class="signal-stat-value tone-up">v' +
      escapeHtml(info.latest_version || "—") +
      "</div></div>";
  }
  const notes = $("#updateModalNotes");
  if (notes) {
    const base = info.notes || "Рекомендуем обновить — установка займёт около минуты.";
    notes.textContent = base + " " + PANEL_UPDATE_AV_HINT;
  }
}

function openUpdateModal(info) {
  if (!info || !info.update_available) return;
  renderUpdateModal(info);
  const modal = $("#updateModal");
  if (modal) modal.hidden = false;
}

function closeUpdateModal() {
  const modal = $("#updateModal");
  if (modal) modal.hidden = true;
}

async function checkPanelUpdate(manual) {
  const btn = $("#btnCheckPanelUpdate");
  if (btn && manual) {
    btn.disabled = true;
    btn.textContent = "Проверка…";
  }
  try {
    const res = await call("check_panel_update");
    renderAboutUpdateUi(res.data);
    if (manual && res.data.update_available) {
      showToast("Доступна версия v" + res.data.latest_version);
    } else if (manual && !res.data.error) {
      showToast("У тебя последняя версия");
    } else if (manual && res.data.error) {
      showToast(res.data.error, "error");
    }
    return res.data;
  } catch (e) {
    if (manual) showToast(String(e.message || e), "error");
    return null;
  } finally {
    if (btn && manual) {
      btn.disabled = false;
      btn.textContent = "Проверить обновления";
    }
  }
}

async function applyPanelUpdate() {
  if (panelUpdateBusy) return;
  panelUpdateBusy = true;
  const buttons = ["#btnInstallPanelUpdate", "#btnUpdateModalApply"];
  buttons.forEach(function (sel) {
    const b = $(sel);
    if (b) {
      b.disabled = true;
      b.textContent = "Скачивание…";
    }
  });
  try {
    await call("apply_panel_update");
    showToast("Обновление установлено — перезапуск…");
  } catch (e) {
    showToast(String(e.message || e), "error");
    panelUpdateBusy = false;
    buttons.forEach(function (sel) {
      const b = $(sel);
      if (b) {
        b.disabled = false;
        b.textContent = sel.indexOf("Modal") >= 0 ? "Обновить (рекомендуется)" : "Обновить сейчас";
      }
    });
  }
}

async function skipPanelUpdate(version) {
  try {
    await call("skip_panel_update", String(version || ""));
  } catch (_) {
    /* ignore */
  }
  closeUpdateModal();
}

async function maybePromptPanelUpdateOnStartup() {
  try {
    const info = await checkPanelUpdate(false);
    if (info && info.update_available && !info.skipped_this_version && info.updatable) {
      openUpdateModal(info);
    }
  } catch (_) {
    /* ignore */
  }
}

function setupPanelUpdateHandlers() {
  const checkBtn = $("#btnCheckPanelUpdate");
  if (checkBtn) {
    checkBtn.addEventListener("click", function () {
      void checkPanelUpdate(true);
    });
  }
  const installBtn = $("#btnInstallPanelUpdate");
  if (installBtn) {
    installBtn.addEventListener("click", function () {
      void applyPanelUpdate();
    });
  }
  const applyModalBtn = $("#btnUpdateModalApply");
  if (applyModalBtn) {
    applyModalBtn.addEventListener("click", function () {
      void applyPanelUpdate();
    });
  }
  const skipBtn = $("#btnUpdateModalSkip");
  if (skipBtn) {
    skipBtn.addEventListener("click", function () {
      const ver = panelUpdateInfo && panelUpdateInfo.latest_version;
      void skipPanelUpdate(ver);
      showToast("Обновление отложено");
    });
  }
  const closeBtn = $("#updateModalClose");
  const backdrop = $("#updateModalBackdrop");
  if (closeBtn) closeBtn.addEventListener("click", closeUpdateModal);
  if (backdrop) backdrop.addEventListener("click", closeUpdateModal);
  const siteLink = $("#aboutSiteLink");
  if (siteLink) {
    siteLink.addEventListener("click", function (e) {
      e.preventDefault();
      if (api && api.open_external) {
        void call("open_external", "https://axon-trade.com/ru/radar/panel");
      }
    });
  }
}

function navigateToSection(section) {
  const item = document.querySelector('.nav-item[data-section="' + section + '"]');
  if (!item) return;
  $all(".nav-item").forEach(function (i) { i.classList.remove("active"); });
  item.classList.add("active");
  $all(".section").forEach(function (s) { s.classList.remove("active"); });
  const secEl = $("#section-" + section);
  if (secEl) secEl.classList.add("active");
  if (section === "signals") renderSignals();
  if (section === "ban") renderBanList();
  if (section === "analyzer" || section === "profile") renderProfileUi();
  if (section === "about") {
    void loadPanelVersionInfo();
    void checkPanelUpdate(false);
    void loadAutostartState();
  }
  if (section === "settings") loadSettings();
  if (section === "logs") renderLogs();
  if (section === "deploy") {
    loadDeployManifest();
    renderDeployQueue();
  }
}

function setupNavigation() {
  $all(".nav-item").forEach(function (item) {
    item.addEventListener("click", function () {
      navigateToSection(item.dataset.section);
    });
  });
  const outcomesProfileBtn = $("#btnSignalsOutcomesProfile");
  if (outcomesProfileBtn) {
    outcomesProfileBtn.addEventListener("click", function () {
      navigateToSection("profile");
    });
  }
}

function setupAnalyzerExamples() {
  $all(".analyzer-example-top").forEach(function (btn) {
    btn.addEventListener("click", function () {
      const q = btn.dataset.q || "/top";
      const input = $("#analyzerQuery");
      if (input) {
        input.value = q;
        input.focus();
      }
      runAnalyzer();
    });
  });
}

function createTestTracking() {
  const now = Math.floor(Date.now() / 1000);
  return {
    signal_id: "test-track-" + now,
    symbol: "PEPE/USDT",
    tp_hit: 0,
    sl_hit: false,
    bu_hit: false,
    tracking_enabled: true,
    is_test: true,
  };
}

function fireTestTracking() {
  if (!isRealAdmin()) return;
  localTestTracking = createTestTracking();
  void renderTracking();
  showToast("Тест сопровождения добавлен — переключай «Ведём» / «Выкл»");
}

function isTestTrackingId(signalId) {
  return String(signalId || "").indexOf("test-track-") === 0;
}

function mergeTrackingItems(apiItems) {
  let items = (apiItems || []).slice();
  if (localTestTracking) {
    items = [localTestTracking].concat(
      items.filter(function (t) {
        return t.signal_id !== localTestTracking.signal_id;
      })
    );
  }
  return items;
}

function trackingItemsForUser(items) {
  if (isAdminUi()) return items;
  return items.filter(function (t) {
    return t.tracking_enabled !== false;
  });
}

function setupHandlers() {
  const notifySoundToggle = $("#toggleNotifySound");
  if (notifySoundToggle) {
    notifySoundToggle.addEventListener("change", async function (e) {
      try {
        await call("save_notify_sound", e.target.checked);
        showToast(e.target.checked ? "Звук уведомлений включён" : "Звук уведомлений выключен");
      } catch (err) {
        showToast(String(err.message || err), "error");
        e.target.checked = !e.target.checked;
      }
    });
  }

  const autostartToggle = $("#toggleAutostart");
  if (autostartToggle) {
    autostartToggle.addEventListener("change", async function (e) {
      const want = e.target.checked;
      try {
        const res = await call("set_autostart", want);
        if (!res || res.ok === false) {
          throw new Error((res && res.error) || "Не удалось изменить автозапуск");
        }
        const enabled = res.data && res.data.enabled === true;
        e.target.checked = enabled;
        showToast(enabled ? "Автозапуск с Windows включён" : "Автозапуск с Windows выключен");
      } catch (err) {
        e.target.checked = !want;
        showToast(String(err.message || err), "error");
      }
    });
  }

  const btnTestNotifySignal = $("#btnTestNotifySignal");
  if (btnTestNotifySignal) {
    btnTestNotifySignal.addEventListener("click", function () {
      fireTestSignal();
    });
  }

  const btnTestNotifyAnalyzer = $("#btnTestNotifyAnalyzer");
  if (btnTestNotifyAnalyzer) {
    btnTestNotifyAnalyzer.addEventListener("click", function () {
      deliverNotification(
        "Анализер готов: BTC",
        "BTC — открой вкладку Анализер",
        "analyzer",
        "analyzer",
        "Открыть результат",
        function () {
          openAnalyzerSection();
        },
        10000
      );
      showToast("Отправлен тест анализера");
    });
  }

  const btnTestWindowsToast = $("#btnTestWindowsToast");
  if (btnTestWindowsToast) {
    btnTestWindowsToast.addEventListener("click", function () {
      call("test_windows_toast").then(function () {
        showToast("Windows toast отправлен");
      }).catch(function (e) {
        showToast(String(e.message || e), "error");
      });
    });
  }

  const btnPreviewMember = $("#btnPreviewMember");
  if (btnPreviewMember) {
    btnPreviewMember.addEventListener("click", function () {
      enterMemberPreview();
    });
  }

  const btnExitPreview = $("#btnExitPreview");
  if (btnExitPreview) {
    btnExitPreview.addEventListener("click", function () {
      exitMemberPreview();
    });
  }

  const btnTestTracking = $("#btnTestTracking");
  if (btnTestTracking) {
    btnTestTracking.addEventListener("click", function () {
      fireTestTracking();
    });
  }

  $("#toggleTelegram").addEventListener("change", async function (e) {
    toggling = true;
    try {
      await call("update_settings", { signals_enabled: e.target.checked });
      $("#tgStatusVal").textContent = e.target.checked ? "Включено" : "Выключено";
      showToast(e.target.checked ? "Telegram включён" : "Telegram выключен");
    } catch (err) {
      showToast(String(err.message || err), "error");
      e.target.checked = !e.target.checked;
    } finally {
      toggling = false;
    }
  });

  $("#toggleHeartbeat").addEventListener("change", async function (e) {
    toggling = true;
    try {
      await call("update_settings", { heartbeat_enabled: e.target.checked });
      showToast(e.target.checked ? "Heartbeat включён" : "Heartbeat выключен");
    } catch (err) {
      showToast(String(err.message || err), "error");
      e.target.checked = !e.target.checked;
    } finally {
      toggling = false;
    }
  });

  const toggleStartupGroup = $("#toggleStartupGroup");
  if (toggleStartupGroup) {
    toggleStartupGroup.addEventListener("change", async function (e) {
      toggling = true;
      try {
        await call("update_settings", { startup_group_enabled: e.target.checked });
        showToast(
          e.target.checked
            ? "Приветствие в группу включено"
            : "Приветствие в группу выключено — перезапуск незаметен для группы"
        );
      } catch (err) {
        showToast(String(err.message || err), "error");
        e.target.checked = !e.target.checked;
      } finally {
        toggling = false;
      }
    });
  }

  $("#btnRefreshOverview").addEventListener("click", async function () {
    await refreshOverview();
    showToast("Данные обновлены");
  });

  $("#btnRestartBot").addEventListener("click", async function () {
    const btn = this;
    btn.disabled = true;
    const original = btn.innerHTML;
    btn.innerHTML = "Перезапуск…";
    try {
      await call("restart");
      showToast("Бот перезапускается");
      setTimeout(refreshOverview, 4000);
    } catch (e) {
      showToast(String(e.message || e), "error");
    } finally {
      btn.disabled = false;
      btn.innerHTML = original;
    }
  });

  $("#btnReconnectOverview").addEventListener("click", async function () {
    await refreshOverview();
    if (connected) showToast("Подключено к серверу");
    else showToast("Не удалось подключиться", "error");
  });

  $("#signalsPeriod").addEventListener("change", renderSignals);
  $("#btnRefreshSignals").addEventListener("click", function () {
    renderSignals();
    showToast("Список сигналов обновлён");
  });

  $("#addBanForm").addEventListener("submit", async function (e) {
    e.preventDefault();
    const ticker = $("#banTicker").value.trim();
    const reason = $("#banReason").value.trim();
    if (!ticker) return;
    try {
      await call("add_ban", ticker, reason);
      $("#banTicker").value = "";
      $("#banReason").value = "";
      renderBanList();
      showToast("Добавлено: " + ticker.toUpperCase());
    } catch (err) {
      showToast(String(err.message || err), "error");
    }
  });

  $("#greetingText").addEventListener("input", renderPreview);
  $("#btnSaveSettings").addEventListener("click", async function () {
    try {
      await call("update_settings", { startup_message: $("#greetingText").value });
      showToast("Приветствие сохранено. Перезапусти бота, чтобы увидеть в Telegram.");
    } catch (e) {
      showToast(String(e.message || e), "error");
    }
  });

  $("#btnRefreshLogs").addEventListener("click", function () {
    renderLogs();
    showToast("Логи обновлены");
  });

  $("#btnConnect").addEventListener("click", async function () {
    try {
      telegramBotUsername = ($("#connTgBot") && $("#connTgBot").value.trim()) || "";
      await call(
        "save_config",
        $("#connIp").value.trim(),
        parseInt($("#connPort").value.trim(), 10) || 8765,
        $("#connPass").value.trim(),
        telegramBotUsername
      );
      await refreshOverview();
      mountTelegramLogin();
      showToast("Подключение сохранено");
    } catch (e) {
      showToast(String(e.message || e), "error");
    }
  });

  const btnAnalyze = $("#btnAnalyze");
  if (btnAnalyze) {
    btnAnalyze.addEventListener("click", runAnalyzer);
  }
  const analyzerQuery = $("#analyzerQuery");
  if (analyzerQuery) {
    analyzerQuery.addEventListener("keydown", function (e) {
      if (e.key === "Enter") runAnalyzer();
    });
  }
  const btnRadarRefresh = $("#btnRadarRefresh");
  if (btnRadarRefresh) {
    btnRadarRefresh.addEventListener("click", async function () {
      try {
        const res = await call("radar_refresh");
        radarUser = res.data || radarUser;
        applyAdminUi();
        renderProfileUi();
        renderSignals();
        showToast(profileRefreshToast(radarUser));
      } catch (e) {
        showToast(String(e.message || e), "error");
      }
    });
  }
  const btnRadarLogout = $("#btnRadarLogout");
  if (btnRadarLogout) {
    btnRadarLogout.addEventListener("click", function () {
      void logoutRadar();
    });
  }
  const btnSiteAuth = $("#btnSiteAuth");
  if (btnSiteAuth) {
    btnSiteAuth.addEventListener("click", authViaSite);
  }

  $("#btnDisconnect").addEventListener("click", function () {
    setConnected(false);
    showToast("Отключено (локально)");
  });

  $("#togglePass").addEventListener("click", function () {
    const inp = $("#connPass");
    inp.type = inp.type === "password" ? "text" : "password";
  });

  const modalClose = $("#signalModalClose");
  const modalBackdrop = $("#signalModalBackdrop");
  if (modalClose) modalClose.addEventListener("click", closeSignalModal);
  if (modalBackdrop) modalBackdrop.addEventListener("click", closeSignalModal);

  const tpHitModalClose = $("#tpHitModalClose");
  const tpHitModalBackdrop = $("#tpHitModalBackdrop");
  const tpHitModalOk = $("#tpHitModalOk");
  if (tpHitModalClose) tpHitModalClose.addEventListener("click", closeTpHitModal);
  if (tpHitModalBackdrop) tpHitModalBackdrop.addEventListener("click", closeTpHitModal);
  if (tpHitModalOk) tpHitModalOk.addEventListener("click", closeTpHitModal);

  const btnBingx = $("#btnOpenBingx");
  if (btnBingx) {
    btnBingx.addEventListener("click", async function () {
      if (!signalModalBingxUrl) return;
      try {
        await call("open_url", signalModalBingxUrl);
      } catch (e) {
        window.open(signalModalBingxUrl, "_blank");
      }
    });
  }

  const btnRefreshChart = $("#btnRefreshSignalChart");
  if (btnRefreshChart) {
    btnRefreshChart.addEventListener("click", async function () {
      if (!signalModalId) return;
      try {
        await loadSignalChart(signalModalId, signalChartTf);
        showToast("График обновлён");
      } catch (e) {
        showToast(String(e.message || e), "error");
      }
    });
  }

  const btnMeasure = $("#btnChartMeasure");
  if (btnMeasure) {
    btnMeasure.addEventListener("click", function () {
      setMeasureMode(!measureMode);
    });
  }
  setupChartRulerDrag();

  $all(".signal-tf", $("#signalTfBar")).forEach(function (btn) {
    if (!btn.dataset.tf) return;
    btn.addEventListener("click", async function () {
      if (!signalModalId) return;
      const tf = btn.dataset.tf || "1m";
      btn.disabled = true;
      try {
        await loadSignalChart(signalModalId, tf, false);
        startSignalLivePolling();
      } catch (e) {
        showToast(String(e.message || e), "error");
      } finally {
        btn.disabled = false;
      }
    });
  });

  document.addEventListener("keydown", function (e) {
    if (e.key === "Escape" && signalModalId) closeSignalModal();
  });
}

async function pollSignalNotifications() {
  if (!connected || !radarUser) return;
  if (!radarUser.has_access && !radarUser.is_admin) return;
  try {
    const res = await call("radar_signals", 24, 1);
    const rows = res.data || [];
    if (!rows.length) return;
    const s = rows[0];
    const key = String(s.signal_id || signalIdFromRow(s.symbol, s.timestamp) || s.symbol + "-" + s.timestamp);

    if (!signalNotifySessionReady) {
      signalNotifySessionReady = true;
      lastSignalNotifyKey = key;
      return;
    }

    if (key === lastSignalNotifyKey) return;
    lastSignalNotifyKey = key;
    notifyNewSignal(s);
    if (document.querySelector("#section-signals.active")) {
      await renderSignals();
    }
  } catch (_) {}
}

async function pollTrackingTpNotifications() {
  if (!connected || !radarUser) return;
  if (!radarUser.has_access && !radarUser.is_admin) return;
  try {
    const res = await call("radar_tracking");
    const items = res.data || [];

    if (!trackingTpSessionReady) {
      trackingTpSessionReady = true;
      items.forEach(function (t) {
        if (t && t.signal_id) {
          trackingTpState[String(t.signal_id)] = Number(t.tp_hit || 0);
        }
      });
      return;
    }

    items.forEach(function (t) {
      if (!t || !t.signal_id) return;
      const id = String(t.signal_id);
      const tp = Number(t.tp_hit || 0);
      const prev = Number(trackingTpState[id] || 0);
      // Только следующий TP в реальном времени — не догоняем старые (0→2, вчерашние и т.д.)
      if (tp === prev + 1 && tp > 0) {
        notifyTpHit(t, tp);
      }
      trackingTpState[id] = tp;
    });

    if (document.querySelector("#section-signals.active")) {
      await renderTracking();
    }
  } catch (_) {}
}

async function init() {
  setupNavigation();
  setupHandlers();
  setupPanelUpdateHandlers();
  setupAnalyzerExamples();
  setupDeployHandlers();
  applyAdminUi();
  syncPanelForeground();
  document.addEventListener("visibilitychange", syncPanelForeground);
  window.addEventListener("focus", syncPanelForeground);
  window.addEventListener("blur", syncPanelForeground);
  hideSplash();
  try {
    const ready = await waitApi();
    if (!ready) {
      showToast("API не подключился — перезапусти панель", "error");
    } else {
      try {
        await loadConfigForm();
      } catch (e) {
        /* ok */
      }
      void loadAutostartState();
      await loadPanelVersionInfo();
      void maybePromptPanelUpdateOnStartup();
      setInterval(function () {
        if (!panelUpdateBusy) {
          void checkPanelUpdate(false).then(function (info) {
            if (
              info &&
              info.update_available &&
              !info.skipped_this_version &&
              isPanelBackground()
            ) {
              deliverNotification(
                "Доступно обновление панели",
                "v" + info.latest_version + " — нажми, чтобы открыть «О программе»",
                "default",
                "about",
                "Обновить",
                function () {
                  const nav = document.querySelector('.nav-item[data-section="about"]');
                  if (nav) nav.click();
                  openUpdateModal(info);
                },
                20000
              );
            }
          });
        }
      }, 6 * 60 * 60 * 1000);
      void refreshOverview(true).then(function () {
        pollTimer = setInterval(function () { refreshOverview(false); }, 15000);
        setInterval(pollNotifyUiActions, 600);
        pollNotifyUiActions();
        return ensureRadarSession();
      }).then(function () {
        resetNotifySessionBaseline();
        applyAdminUi();
        renderProfileUi();
        renderSignals();
        setInterval(pollSignalNotifications, 12000);
        setInterval(pollTrackingTpNotifications, 12000);
        pollSignalNotifications();
        pollTrackingTpNotifications();
        setInterval(function () {
          if (connected) void call("telemetry_event", "heartbeat");
        }, 30 * 60 * 1000);
      });
    }
  } catch (e) {
    showToast(String(e.message || e), "error");
  }
  setInterval(function () {
    if (connected) {
      const elapsed = Math.floor((Date.now() - uptimeSyncedAt) / 1000);
      $("#metricUptime").textContent = formatUptime(uptimeSeconds + elapsed);
    }
  }, 1000);
}

init();
