Files
parse_link_vpn/public/app.js
2026-06-04 00:14:54 +05:00

315 lines
9.2 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 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 '<span class="latency-cell latency-error">ошибка</span>';
if (latency < 100) return `<span class="latency-cell latency-good">${latency} мс</span>`;
if (latency < 300) return `<span class="latency-cell latency-fair">${latency} мс</span>`;
return `<span class="latency-cell latency-poor">${latency} мс</span>`;
}
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 = '<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, pingList) {
const tbody = $(`#${prefix}-tbody`);
const colspan = 6;
if (!outbounds.length) {
tbody.innerHTML = `<tr><td colspan="${colspan}" style="color:var(--text-muted);text-align:center;padding:2rem;">Нет данных</td></tr>`;
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
? '<span class="synced-badge yes">да</span>'
: '<span class="synced-badge no">—</span>';
let latencyCell = '<span class="latency-cell latency-error">—</span>';
if (pingMap[tag]) {
latencyCell = formatLatency(pingMap[tag].latency);
}
const latencyCol = `<td>${latencyCell}</td>`;
return `<tr><td>${tag}</td><td>${proto}</td><td>${addr}</td><td>${port}</td>${latencyCol}<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([]);
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(() => {
fetchStatus();
fetchNovavpsData();
fetchXrayData([]);
}, 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({ subscriptionUrl: subscriptionUrlInput.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));