diff --git a/public/app.js b/public/app.js index 0057200..4a62f6d 100644 --- a/public/app.js +++ b/public/app.js @@ -13,10 +13,12 @@ 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"); @@ -27,25 +29,46 @@ function showScreen(screen) { function formatTime(iso) { if (!iso) return ""; const d = new Date(iso); - return d.toLocaleString(); + 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`); - badge.textContent = data.status; + 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} connections`; + detailText = `${data.count} подключеній`; } else if (data.outboundsCount != null) { - detailText = `${data.outboundsCount} outbounds`; - if (data.synced) detailText += " (synced)"; + detailText = `${data.outboundsCount} исходящихъ`; + if (data.synced) detailText += " · синхр."; } - if (data.error) detailText += (detailText ? " | " : "") + data.error; + if (data.error) detailText += (detailText ? " · " : "") + data.error; detail.textContent = detailText; time.textContent = formatTime(data.timestamp); } @@ -63,26 +86,17 @@ async function fetchStatus() { const isRunning = data.status === "running"; runBtn.disabled = isRunning; runningIndicator.classList.toggle("hidden", !isRunning); - - const stepLabels = { - initializing: "Initializing...", - novavps_fetching: "Fetching Novavps data...", - xray1_parsing: "Parsing Xray 1...", - xray1_syncing: "Syncing Xray 1...", - xray2_parsing: "Parsing Xray 2...", - xray2_syncing: "Syncing Xray 2...", - done: "Completed", - novavps_url_missing: "Error: NOVAVPS URL not set", - no_xray_panels: "Error: No Xray panels configured", - fatal: "Fatal error", - }; 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) : "Never"; + 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); } @@ -103,11 +117,90 @@ async function fetchConfig() { } } +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 = 'Нѣтъ данныхъ'; + 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) { + const tbody = $(`#${prefix}-tbody`); + if (!outbounds.length) { + tbody.innerHTML = 'Нѣтъ данныхъ'; + return; + } + 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 + ? 'да' + : '—'; + return `${tag}${proto}${addr}${port}${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, POLL_INTERVAL); + pollTimer = setInterval(() => { + fetchStatus(); + fetchNovavpsData(); + fetchXrayData("xray1", []); + fetchXrayData("xray2", []); + }, POLL_INTERVAL); } function stopPolling() { @@ -124,6 +217,13 @@ function showMsg(el, text, type) { 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"); @@ -139,14 +239,14 @@ loginForm.addEventListener("submit", async (e) => { }); const data = await res.json(); if (!res.ok) { - loginError.textContent = data.error || "Login failed"; + loginError.textContent = data.error || "Входъ невозможенъ"; loginError.classList.remove("hidden"); return; } showScreen(dashboardScreen); startPolling(); } catch (err) { - loginError.textContent = "Network error"; + loginError.textContent = "Ошибка сѣти"; loginError.classList.remove("hidden"); } }); @@ -168,12 +268,12 @@ saveConfigBtn.addEventListener("click", async () => { }); const data = await res.json(); if (!res.ok) { - showMsg(configMsg, data.error || "Save failed", "error"); + showMsg(configMsg, data.error || "Ошибка сохраненія", "error"); return; } - showMsg(configMsg, "Saved", "success"); + showMsg(configMsg, "Сохранено", "success"); } catch (err) { - showMsg(configMsg, "Network error", "error"); + showMsg(configMsg, "Ошибка сѣти", "error"); } }); @@ -183,17 +283,16 @@ runBtn.addEventListener("click", async () => { const res = await fetch("/api/run", { method: "POST" }); const data = await res.json(); if (!res.ok) { - showMsg(runMsg, data.error || "Failed to start", "error"); + showMsg(runMsg, data.error || "Не удалось запустить", "error"); return; } - showMsg(runMsg, "Parser started", "success"); + showMsg(runMsg, "Парсеръ запущенъ", "success"); fetchStatus(); } catch (err) { - showMsg(runMsg, "Network error", "error"); + showMsg(runMsg, "Ошибка сѣти", "error"); } }); -// Check if already logged in fetch("/api/status") .then((res) => { if (res.ok) { diff --git a/public/index.html b/public/index.html index 6eb5420..95f0506 100644 --- a/public/index.html +++ b/public/index.html @@ -1,85 +1,178 @@ - + - VPN Parser + VPN Parser — Панель управленія
-