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 subscriptionUrlInput = $("#subscription-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 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: "Получение данных подписки...", parsing: "Чтение панели Xray...", syncing: "Синхронизация Xray...", pinging: "Проверка соединений (Xray)...", done: "Завершено", subscription_url_missing: "Ошибка: ссылка подписки не задана", no_xray_panel: "Ошибка: не настроена панель 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"; runBtn.disabled = isRunning; runningIndicator.classList.toggle("hidden", !isRunning); currentStep.textContent = stepLabels[data.currentStep] || data.currentStep; updatePanelStatus("novavps", data.novavps); updatePanelStatus("xray", data.xray); pingResults = data.xray?.pingResults || []; lastRunEl.textContent = data.lastRun ? formatTime(data.lastRun) : "Никогда"; } 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(); subscriptionUrlInput.value = data.subscriptionUrl || ""; } 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(syncedTags) { try { const res = await fetch("/api/data/xray"); if (res.status === 401) return; const data = await res.json(); renderXrayTable("xray", data, syncedTags || [], pingResults); $("#xray-data-count").textContent = data.length; } catch {} } function renderNovavpsTable(connections) { const tbody = $("#novavps-tbody"); if (!connections.length) { tbody.innerHTML = '