This commit is contained in:
2026-06-04 00:14:54 +05:00
parent a0a6135d2c
commit 85afc04ce2
11 changed files with 178 additions and 411 deletions

View File

@@ -5,16 +5,14 @@ const dashboardScreen = $("#dashboard-screen");
const loginForm = $("#login-form");
const loginError = $("#login-error");
const logoutBtn = $("#logout-btn");
const novavpsUrlInput = $("#novavps-url");
const subscriptionUrlInput = $("#subscription-url");
const saveConfigBtn = $("#save-config-btn");
const configMsg = $("#config-msg");
const runBtn = $("#run-btn");
const syncXray2Btn = $("#sync-xray2-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;
@@ -42,16 +40,13 @@ function formatTime(iso) {
const stepLabels = {
initializing: "Инициализация...",
novavps_fetching: "Получение данных NOVAVPS...",
xray1_parsing: "Чтение панели Xray 1...",
xray1_syncing: "Синхронизация Xray 1...",
pinging: "Проверка соединений (Xray 1)...",
xray2_parsing: "Чтение панели Xray 2...",
xray2_syncing: "Синхронизация Xray 2...",
xray2_pinging: "Проверка соединений (Xray 2)...",
novavps_fetching: "Получение данных подписки...",
parsing: "Чтение панели Xray...",
syncing: "Синхронизация Xray...",
pinging: "Проверка соединений (Xray)...",
done: "Завершено",
novavps_url_missing: "Ошибка: ссылка NOVAVPS не задана",
no_xray_panels: "Ошибка: нет настроенных панелей Xray",
subscription_url_missing: "Ошибка: ссылка подписки не задана",
no_xray_panel: "Ошибка: не настроена панель Xray",
fatal: "Критическая ошибка",
};
@@ -94,26 +89,18 @@ async function fetchStatus() {
const data = await res.json();
const isRunning = data.status === "running";
const isXray2Syncing = data.currentStep === "xray2_syncing";
runBtn.disabled = isRunning;
syncXray2Btn.disabled = isRunning || isXray2Syncing;
runningIndicator.classList.toggle("hidden", !isRunning);
currentStep.textContent = stepLabels[data.currentStep] || data.currentStep;
const hasXray2 = data.configuredPanels && data.configuredPanels.includes("xray2");
syncXray2Btn.classList.toggle("hidden", !hasXray2);
updatePanelStatus("novavps", data.novavps);
updatePanelStatus("xray1", data.xray1);
updatePanelStatus("xray2", data.xray2);
updatePanelStatus("xray", data.xray);
pingResults = data.xray1?.pingResults || [];
pingResults = data.xray?.pingResults || [];
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);
}
@@ -128,7 +115,7 @@ async function fetchConfig() {
return;
}
const data = await res.json();
novavpsUrlInput.value = data.novavpsUrl || "";
subscriptionUrlInput.value = data.subscriptionUrl || "";
} catch (err) {
console.error("Config fetch error:", err);
}
@@ -144,19 +131,13 @@ async function fetchNovavpsData() {
} catch {}
}
async function fetchXrayData(panel, syncedTags) {
async function fetchXrayData(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 || [], pingResults);
$("#xray1-data-count").textContent = data.length;
} else if (panel === "xray2") {
renderXrayTable("xray2", data, syncedTags || [], []);
$("#xray2-data-count").textContent = data.length;
}
renderXrayTable("xray", data, syncedTags || [], pingResults);
$("#xray-data-count").textContent = data.length;
} catch {}
}
@@ -180,8 +161,7 @@ function renderNovavpsTable(connections) {
function renderXrayTable(prefix, outbounds, syncedTags, pingList) {
const tbody = $(`#${prefix}-tbody`);
const hasLatency = prefix === "xray1";
const colspan = hasLatency ? 6 : 5;
const colspan = 6;
if (!outbounds.length) {
tbody.innerHTML = `<tr><td colspan="${colspan}" style="color:var(--text-muted);text-align:center;padding:2rem;">Нет данных</td></tr>`;
@@ -203,11 +183,11 @@ function renderXrayTable(prefix, outbounds, syncedTags, pingList) {
: '<span class="synced-badge no">—</span>';
let latencyCell = '<span class="latency-cell latency-error">—</span>';
if (hasLatency && pingMap[tag]) {
if (pingMap[tag]) {
latencyCell = formatLatency(pingMap[tag].latency);
}
const latencyCol = hasLatency ? `<td>${latencyCell}</td>` : "";
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("");
@@ -223,14 +203,12 @@ function startPolling() {
fetchStatus();
fetchConfig();
fetchNovavpsData();
fetchXrayData("xray1", []);
fetchXrayData("xray2", []);
fetchXrayData([]);
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(() => {
fetchStatus();
fetchNovavpsData();
fetchXrayData("xray1", []);
fetchXrayData("xray2", []);
fetchXrayData([]);
}, POLL_INTERVAL);
}
@@ -295,7 +273,7 @@ saveConfigBtn.addEventListener("click", async () => {
const res = await fetch("/api/config", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ novavpsUrl: novavpsUrlInput.value }),
body: JSON.stringify({ subscriptionUrl: subscriptionUrlInput.value }),
});
const data = await res.json();
if (!res.ok) {
@@ -324,22 +302,6 @@ runBtn.addEventListener("click", async () => {
}
});
syncXray2Btn.addEventListener("click", async () => {
runMsg.classList.add("hidden");
try {
const res = await fetch("/api/sync-xray2", { method: "POST" });
const data = await res.json();
if (!res.ok) {
showMsg(runMsg, data.error || "Ошибка запуска", "error");
return;
}
showMsg(runMsg, "Синхронизация Xray2 запущена", "success");
fetchStatus();
} catch (err) {
showMsg(runMsg, "Ошибка сети", "error");
}
});
fetch("/api/status")
.then((res) => {
if (res.ok) {

View File

@@ -50,8 +50,8 @@
<section class="card config-card">
<h2>Настройки</h2>
<div class="config-row">
<label for="novavps-url">Ссылка NOVAVPS</label>
<input type="text" id="novavps-url" placeholder="https://novavps.app/s/...">
<label for="subscription-url">Ссылка подписки</label>
<input type="text" id="subscription-url" placeholder="https://novavps.app/s/...">
<button id="save-config-btn" class="btn btn-primary">Сохранить</button>
</div>
<div id="config-msg" class="msg hidden"></div>
@@ -67,17 +67,7 @@
</span>
Обновить сейчас
</button>
<button id="sync-xray2-btn" class="btn btn-secondary btn-lg">
<span class="btn-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
<path d="M3 3v5h5"/>
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/>
<path d="M16 21h5v-5"/>
</svg>
</span>
Синхр. Xray 2
</button>
<div id="run-msg" class="msg hidden"></div>
</section>
@@ -92,7 +82,7 @@
<section class="status-section">
<h2 class="section-title">Статус панелей</h2>
<div class="status-grid">
<div class="status-card" id="xray1-card">
<div class="status-card" id="xray-card">
<div class="status-card-header">
<span class="panel-icon xray-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -102,27 +92,11 @@
<line x1="6" y1="18" x2="6" y2="18"/>
</svg>
</span>
<h3>Xray 1</h3>
<h3>Xray</h3>
</div>
<div class="status-badge" id="xray1-status">ожидание</div>
<div class="status-detail" id="xray1-detail"></div>
<div class="status-time" id="xray1-time"></div>
</div>
<div class="status-card" id="xray2-card">
<div class="status-card-header">
<span class="panel-icon xray-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="2" width="20" height="8" rx="2"/>
<rect x="2" y="14" width="20" height="8" rx="2"/>
<line x1="6" y1="6" x2="6" y2="6"/>
<line x1="6" y1="18" x2="6" y2="18"/>
</svg>
</span>
<h3>Xray 2</h3>
</div>
<div class="status-badge" id="xray2-status">ожидание</div>
<div class="status-detail" id="xray2-detail"></div>
<div class="status-time" id="xray2-time"></div>
<div class="status-badge" id="xray-status">ожидание</div>
<div class="status-detail" id="xray-detail"></div>
<div class="status-time" id="xray-time"></div>
</div>
<div class="status-card" id="novavps-card">
<div class="status-card-header">
@@ -140,15 +114,15 @@
</div>
</section>
<section class="card data-section" id="xray1-data-section">
<div class="data-section-header" onclick="toggleSection('xray1-data')">
<h2>Исходящие Xray 1</h2>
<span class="data-count" id="xray1-data-count"></span>
<span class="collapse-arrow" id="xray1-data-arrow">▼</span>
<section class="card data-section" id="xray-data-section">
<div class="data-section-header" onclick="toggleSection('xray-data')">
<h2>Исходящие Xray</h2>
<span class="data-count" id="xray-data-count"></span>
<span class="collapse-arrow" id="xray-data-arrow">▼</span>
</div>
<div class="data-content" id="xray1-data">
<div class="data-content" id="xray-data">
<div class="table-wrapper">
<table class="data-table" id="xray1-table">
<table class="data-table" id="xray-table">
<thead>
<tr>
<th>Метка</th>
@@ -159,31 +133,7 @@
<th>Синхр.</th>
</tr>
</thead>
<tbody id="xray1-tbody"></tbody>
</table>
</div>
</div>
</section>
<section class="card data-section hidden" id="xray2-data-section">
<div class="data-section-header" onclick="toggleSection('xray2-data')">
<h2>Исходящие Xray 2</h2>
<span class="data-count" id="xray2-data-count"></span>
<span class="collapse-arrow" id="xray2-data-arrow">▼</span>
</div>
<div class="data-content" id="xray2-data">
<div class="table-wrapper">
<table class="data-table" id="xray2-table">
<thead>
<tr>
<th>Метка</th>
<th>Протокол</th>
<th>Адрес</th>
<th>Порт</th>
<th>Синхр.</th>
</tr>
</thead>
<tbody id="xray2-tbody"></tbody>
<tbody id="xray-tbody"></tbody>
</table>
</div>
</div>