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 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 xray2Visible = false; 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...", 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); } 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"; runBtn.disabled = isRunning; runningIndicator.classList.toggle("hidden", !isRunning); currentStep.textContent = stepLabels[data.currentStep] || data.currentStep; updatePanelStatus("novavps", data.novavps); updatePanelStatus("xray1", data.xray1); updatePanelStatus("xray2", data.xray2); 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 || []); $("#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 = '