Files
parse_link_vpn/public/app.js
2026-05-18 19:40:48 +05:00

306 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = '<tr><td colspan="5" style="color:var(--text-muted);text-align:center;padding:2rem;">Нѣтъ данныхъ</td></tr>';
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 `<tr><td>${name}</td><td>${proto}</td><td>${addr}</td><td>${port}</td><td>${sec}</td></tr>`;
})
.join("");
}
function renderXrayTable(prefix, outbounds, syncedTags) {
const tbody = $(`#${prefix}-tbody`);
if (!outbounds.length) {
tbody.innerHTML = '<tr><td colspan="5" style="color:var(--text-muted);text-align:center;padding:2rem;">Нѣтъ данныхъ</td></tr>';
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
? '<span class="synced-badge yes">да</span>'
: '<span class="synced-badge no">—</span>';
return `<tr><td>${tag}</td><td>${proto}</td><td>${addr}</td><td>${port}</td><td>${syncBadge}</td></tr>`;
})
.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");
}
});
fetch("/api/status")
.then((res) => {
if (res.ok) {
showScreen(dashboardScreen);
startPolling();
} else {
showScreen(loginScreen);
}
})
.catch(() => showScreen(loginScreen));