const $ = (sel) => document.querySelector(sel); const loginScreen = $("#login-screen"); const dashboardScreen = $("#dashboard-screen"); const loginForm = $("#login-form"); const loginError = $("#login-error"); const logoutBtn = $("#logout-btn"); const novavpsUrlInput = $("#novavps-url"); const saveConfigBtn = $("#save-config-btn"); const configMsg = $("#config-msg"); const runBtn = $("#run-btn"); const syncXray2Btn = $("#sync-xray2-btn"); const runMsg = $("#run-msg"); const runningIndicator = $("#running-indicator"); const currentStep = $("#current-step"); const lastRunEl = $("#last-run"); const xray2Section = $("#xray2-data-section"); const POLL_INTERVAL = 10000; let pollTimer = null; let pingResults = []; function showScreen(screen) { loginScreen.classList.add("hidden"); dashboardScreen.classList.add("hidden"); screen.classList.remove("hidden"); } function formatTime(iso) { if (!iso) return ""; const d = new Date(iso); return d.toLocaleString("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit", }); } const stepLabels = { initializing: "Инициализация...", novavps_fetching: "Получение данных NOVAVPS...", xray1_parsing: "Чтение панели Xray 1...", xray1_syncing: "Синхронизация Xray 1...", pinging: "Проверка соединений...", xray2_parsing: "Чтение панели Xray 2...", xray2_syncing: "Синхронизация Xray 2...", done: "Завершено", novavps_url_missing: "Ошибка: ссылка NOVAVPS не задана", no_xray_panels: "Ошибка: нет настроенных панелей Xray", fatal: "Критическая ошибка", }; function updatePanelStatus(prefix, data) { const badge = $(`#${prefix}-status`); const detail = $(`#${prefix}-detail`); const time = $(`#${prefix}-time`); const statusMap = { idle: "ожидание", success: "успешно", error: "ошибка" }; badge.textContent = statusMap[data.status] || data.status; badge.className = "status-badge " + data.status; let detailText = ""; if (prefix === "novavps" && data.count != null) { detailText = `${data.count} подключений`; } else if (data.outboundsCount != null) { detailText = `${data.outboundsCount} исходящих`; if (data.synced) detailText += " · синхр."; } if (data.error) detailText += (detailText ? " · " : "") + data.error; detail.textContent = detailText; time.textContent = formatTime(data.timestamp); } function formatLatency(latency) { if (latency == null) return 'ошибка'; if (latency < 100) return `${latency} мс`; if (latency < 300) return `${latency} мс`; return `${latency} мс`; } async function fetchStatus() { try { const res = await fetch("/api/status"); if (res.status === 401) { showScreen(loginScreen); stopPolling(); return; } const data = await res.json(); const isRunning = data.status === "running"; const isXray2Syncing = data.currentStep === "xray2_syncing"; runBtn.disabled = isRunning; syncXray2Btn.disabled = isRunning || isXray2Syncing; runningIndicator.classList.toggle("hidden", !isRunning); currentStep.textContent = stepLabels[data.currentStep] || data.currentStep; updatePanelStatus("novavps", data.novavps); updatePanelStatus("xray1", data.xray1); updatePanelStatus("xray2", data.xray2); pingResults = data.xray1?.pingResults || []; lastRunEl.textContent = data.lastRun ? formatTime(data.lastRun) : "Никогда"; if (data.xray2.status !== "idle" || data.xray2.outboundsCount > 0) { xray2Section.classList.remove("hidden"); } } catch (err) { console.error("Status fetch error:", err); } } async function fetchConfig() { try { const res = await fetch("/api/config"); if (res.status === 401) { showScreen(loginScreen); stopPolling(); return; } const data = await res.json(); novavpsUrlInput.value = data.novavpsUrl || ""; } catch (err) { console.error("Config fetch error:", err); } } async function fetchNovavpsData() { try { const res = await fetch("/api/data/novavps"); if (res.status === 401) return; const data = await res.json(); renderNovavpsTable(data); $("#novavps-data-count").textContent = data.length; } catch {} } async function fetchXrayData(panel, syncedTags) { try { const res = await fetch("/api/data/xray"); if (res.status === 401) return; const data = await res.json(); if (panel === "xray1") { renderXrayTable("xray1", data, syncedTags || [], pingResults); $("#xray1-data-count").textContent = data.length; } else if (panel === "xray2") { renderXrayTable("xray2", data, syncedTags || [], []); $("#xray2-data-count").textContent = data.length; } } catch {} } function renderNovavpsTable(connections) { const tbody = $("#novavps-tbody"); if (!connections.length) { tbody.innerHTML = 'Нет данных'; return; } tbody.innerHTML = connections .map((c) => { const name = escapeHtml(c.name || "—"); const proto = escapeHtml(c.protocol || "—"); const addr = escapeHtml(c.address || "—"); const port = escapeHtml(c.port || "—"); const sec = escapeHtml(c.security || "—"); return `${name}${proto}${addr}${port}${sec}`; }) .join(""); } function renderXrayTable(prefix, outbounds, syncedTags, pingList) { const tbody = $(`#${prefix}-tbody`); const hasLatency = prefix === "xray1"; const colspan = hasLatency ? 6 : 5; if (!outbounds.length) { tbody.innerHTML = `Нет данных`; return; } const pingMap = {}; pingList.forEach((p) => { pingMap[p.tag] = p; }); tbody.innerHTML = outbounds .map((o) => { const tag = escapeHtml(o.remark || o.tag || "—"); const proto = escapeHtml(o.protocol || "—"); const addr = escapeHtml(o.address || "—"); const port = escapeHtml(o.port || "—"); const isSynced = syncedTags.includes(tag); const syncBadge = isSynced ? 'да' : ''; let latencyCell = ''; if (hasLatency && pingMap[tag]) { latencyCell = formatLatency(pingMap[tag].latency); } const latencyCol = hasLatency ? `${latencyCell}` : ""; return `${tag}${proto}${addr}${port}${latencyCol}${syncBadge}`; }) .join(""); } function escapeHtml(str) { const div = document.createElement("div"); div.textContent = str; return div.innerHTML; } function startPolling() { fetchStatus(); fetchConfig(); fetchNovavpsData(); fetchXrayData("xray1", []); fetchXrayData("xray2", []); if (pollTimer) clearInterval(pollTimer); pollTimer = setInterval(() => { fetchStatus(); fetchNovavpsData(); fetchXrayData("xray1", []); fetchXrayData("xray2", []); }, POLL_INTERVAL); } function stopPolling() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } } function showMsg(el, text, type) { el.textContent = text; el.className = "msg " + type; el.classList.remove("hidden"); setTimeout(() => el.classList.add("hidden"), 5000); } window.toggleSection = function(id) { const content = $(`#${id}`); const arrow = $(`#${id}-arrow`); content.classList.toggle("collapsed"); arrow.classList.toggle("collapsed"); }; loginForm.addEventListener("submit", async (e) => { e.preventDefault(); loginError.classList.add("hidden"); const username = $("#username").value; const password = $("#password").value; try { const res = await fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); const data = await res.json(); if (!res.ok) { loginError.textContent = data.error || "Вход невозможен"; loginError.classList.remove("hidden"); return; } showScreen(dashboardScreen); startPolling(); } catch (err) { loginError.textContent = "Ошибка сети"; loginError.classList.remove("hidden"); } }); logoutBtn.addEventListener("click", async () => { try { await fetch("/api/logout", { method: "POST" }); } catch {} stopPolling(); showScreen(loginScreen); }); saveConfigBtn.addEventListener("click", async () => { try { const res = await fetch("/api/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ novavpsUrl: novavpsUrlInput.value }), }); const data = await res.json(); if (!res.ok) { showMsg(configMsg, data.error || "Ошибка сохранения", "error"); return; } showMsg(configMsg, "Сохранено", "success"); } catch (err) { showMsg(configMsg, "Ошибка сети", "error"); } }); runBtn.addEventListener("click", async () => { runMsg.classList.add("hidden"); try { const res = await fetch("/api/run", { method: "POST" }); const data = await res.json(); if (!res.ok) { showMsg(runMsg, data.error || "Не удалось запустить", "error"); return; } showMsg(runMsg, "Парсер запущен", "success"); fetchStatus(); } catch (err) { showMsg(runMsg, "Ошибка сети", "error"); } }); syncXray2Btn.addEventListener("click", async () => { runMsg.classList.add("hidden"); try { const res = await fetch("/api/sync-xray2", { method: "POST" }); const data = await res.json(); if (!res.ok) { showMsg(runMsg, data.error || "Ошибка запуска", "error"); return; } showMsg(runMsg, "Синхронизация Xray2 запущена", "success"); fetchStatus(); } catch (err) { showMsg(runMsg, "Ошибка сети", "error"); } }); fetch("/api/status") .then((res) => { if (res.ok) { showScreen(dashboardScreen); startPolling(); } else { showScreen(loginScreen); } }) .catch(() => showScreen(loginScreen));