Compare commits
2 Commits
3d9c3ad6b7
...
22968084b4
| Author | SHA1 | Date | |
|---|---|---|---|
| 22968084b4 | |||
| 0cb111d6d3 |
@@ -18,7 +18,7 @@ const xray2Section = $("#xray2-data-section");
|
||||
const POLL_INTERVAL = 10000;
|
||||
|
||||
let pollTimer = null;
|
||||
let xray2Visible = false;
|
||||
let pingResults = [];
|
||||
|
||||
function showScreen(screen) {
|
||||
loginScreen.classList.add("hidden");
|
||||
@@ -40,15 +40,16 @@ function formatTime(iso) {
|
||||
}
|
||||
|
||||
const stepLabels = {
|
||||
initializing: "Иніціализація...",
|
||||
novavps_fetching: "Полученіе данныхъ NOVAVPS...",
|
||||
xray1_parsing: "Чтеніе панели Xray 1...",
|
||||
xray1_syncing: "Синхронизація Xray 1...",
|
||||
xray2_parsing: "Чтеніе панели Xray 2...",
|
||||
xray2_syncing: "Синхронизація Xray 2...",
|
||||
initializing: "Инициализация...",
|
||||
novavps_fetching: "Получение данных NOVAVPS...",
|
||||
xray1_parsing: "Чтение панели Xray 1...",
|
||||
xray1_syncing: "Синхронизация Xray 1...",
|
||||
pinging: "Проверка соединений...",
|
||||
xray2_parsing: "Чтение панели Xray 2...",
|
||||
xray2_syncing: "Синхронизация Xray 2...",
|
||||
done: "Завершено",
|
||||
novavps_url_missing: "Ошибка: ссылка NOVAVPS не задана",
|
||||
no_xray_panels: "Ошибка: нѣтъ настроенныхъ панелей Xray",
|
||||
no_xray_panels: "Ошибка: нет настроенных панелей Xray",
|
||||
fatal: "Критическая ошибка",
|
||||
};
|
||||
|
||||
@@ -57,15 +58,15 @@ function updatePanelStatus(prefix, data) {
|
||||
const detail = $(`#${prefix}-detail`);
|
||||
const time = $(`#${prefix}-time`);
|
||||
|
||||
const statusMap = { idle: "ожиданіе", success: "успѣшно", error: "ошибка" };
|
||||
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} подключеній`;
|
||||
detailText = `${data.count} подключений`;
|
||||
} else if (data.outboundsCount != null) {
|
||||
detailText = `${data.outboundsCount} исходящихъ`;
|
||||
detailText = `${data.outboundsCount} исходящих`;
|
||||
if (data.synced) detailText += " · синхр.";
|
||||
}
|
||||
if (data.error) detailText += (detailText ? " · " : "") + data.error;
|
||||
@@ -73,6 +74,13 @@ function updatePanelStatus(prefix, data) {
|
||||
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");
|
||||
@@ -92,6 +100,8 @@ async function fetchStatus() {
|
||||
updatePanelStatus("xray1", data.xray1);
|
||||
updatePanelStatus("xray2", data.xray2);
|
||||
|
||||
pingResults = data.xray1?.pingResults || [];
|
||||
|
||||
lastRunEl.textContent = data.lastRun ? formatTime(data.lastRun) : "Никогда";
|
||||
|
||||
if (data.xray2.status !== "idle" || data.xray2.outboundsCount > 0) {
|
||||
@@ -134,10 +144,10 @@ async function fetchXrayData(panel, syncedTags) {
|
||||
const data = await res.json();
|
||||
|
||||
if (panel === "xray1") {
|
||||
renderXrayTable("xray1", data, syncedTags || []);
|
||||
renderXrayTable("xray1", data, syncedTags || [], pingResults);
|
||||
$("#xray1-data-count").textContent = data.length;
|
||||
} else if (panel === "xray2") {
|
||||
renderXrayTable("xray2", data, syncedTags || []);
|
||||
renderXrayTable("xray2", data, syncedTags || [], []);
|
||||
$("#xray2-data-count").textContent = data.length;
|
||||
}
|
||||
} catch {}
|
||||
@@ -146,7 +156,7 @@ async function fetchXrayData(panel, syncedTags) {
|
||||
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>';
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="color:var(--text-muted);text-align:center;padding:2rem;">Нет данных</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = connections
|
||||
@@ -161,12 +171,19 @@ function renderNovavpsTable(connections) {
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderXrayTable(prefix, outbounds, syncedTags) {
|
||||
function renderXrayTable(prefix, outbounds, syncedTags, pingList) {
|
||||
const tbody = $(`#${prefix}-tbody`);
|
||||
const hasLatency = prefix === "xray1";
|
||||
const colspan = hasLatency ? 6 : 5;
|
||||
|
||||
if (!outbounds.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="color:var(--text-muted);text-align:center;padding:2rem;">Нѣтъ данныхъ</td></tr>';
|
||||
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 || "—");
|
||||
@@ -177,7 +194,14 @@ function renderXrayTable(prefix, outbounds, syncedTags) {
|
||||
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>`;
|
||||
|
||||
let latencyCell = '<span class="latency-cell latency-error">—</span>';
|
||||
if (hasLatency && pingMap[tag]) {
|
||||
latencyCell = formatLatency(pingMap[tag].latency);
|
||||
}
|
||||
|
||||
const latencyCol = hasLatency ? `<td>${latencyCell}</td>` : "";
|
||||
return `<tr><td>${tag}</td><td>${proto}</td><td>${addr}</td><td>${port}</td>${latencyCol}<td>${syncBadge}</td></tr>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
@@ -239,14 +263,14 @@ loginForm.addEventListener("submit", async (e) => {
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
loginError.textContent = data.error || "Входъ невозможенъ";
|
||||
loginError.textContent = data.error || "Вход невозможен";
|
||||
loginError.classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
showScreen(dashboardScreen);
|
||||
startPolling();
|
||||
} catch (err) {
|
||||
loginError.textContent = "Ошибка сѣти";
|
||||
loginError.textContent = "Ошибка сети";
|
||||
loginError.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
@@ -268,12 +292,12 @@ saveConfigBtn.addEventListener("click", async () => {
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
showMsg(configMsg, data.error || "Ошибка сохраненія", "error");
|
||||
showMsg(configMsg, data.error || "Ошибка сохранения", "error");
|
||||
return;
|
||||
}
|
||||
showMsg(configMsg, "Сохранено", "success");
|
||||
} catch (err) {
|
||||
showMsg(configMsg, "Ошибка сѣти", "error");
|
||||
showMsg(configMsg, "Ошибка сети", "error");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -286,10 +310,10 @@ runBtn.addEventListener("click", async () => {
|
||||
showMsg(runMsg, data.error || "Не удалось запустить", "error");
|
||||
return;
|
||||
}
|
||||
showMsg(runMsg, "Парсеръ запущенъ", "success");
|
||||
showMsg(runMsg, "Парсер запущен", "success");
|
||||
fetchStatus();
|
||||
} catch (err) {
|
||||
showMsg(runMsg, "Ошибка сѣти", "error");
|
||||
showMsg(runMsg, "Ошибка сети", "error");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VPN Parser — Панель управленія</title>
|
||||
<title>VPN Parser — Панель управления</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -33,9 +33,9 @@
|
||||
<header class="topbar">
|
||||
<div class="topbar-left">
|
||||
<div class="logo-icon small"></div>
|
||||
<span class="topbar-title">VPN Parser — Панель управленія</span>
|
||||
<span class="topbar-title">VPN Parser — Панель управления</span>
|
||||
</div>
|
||||
<button id="logout-btn" class="btn btn-ghost">Выходъ</button>
|
||||
<button id="logout-btn" class="btn btn-ghost">Выход</button>
|
||||
</header>
|
||||
|
||||
<main class="dashboard">
|
||||
@@ -52,7 +52,7 @@
|
||||
<section class="card actions-card">
|
||||
<button id="run-btn" class="btn btn-success btn-lg">
|
||||
<span class="btn-icon">⟳</span>
|
||||
Обновить сейчасъ
|
||||
Обновить сейчас
|
||||
</button>
|
||||
<div id="run-msg" class="msg hidden"></div>
|
||||
</section>
|
||||
@@ -60,20 +60,20 @@
|
||||
<section id="running-indicator" class="card running-card hidden">
|
||||
<div class="spinner"></div>
|
||||
<div class="running-text">
|
||||
<span class="running-label">Выполненіе</span>
|
||||
<span id="current-step" class="running-step">Иніціализація...</span>
|
||||
<span class="running-label">Выполнение</span>
|
||||
<span id="current-step" class="running-step">Инициализация...</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="status-section">
|
||||
<h2 class="section-title">Статусъ панелей</h2>
|
||||
<h2 class="section-title">Статус панелей</h2>
|
||||
<div class="status-grid">
|
||||
<div class="status-card" id="novavps-card">
|
||||
<div class="status-card-header">
|
||||
<span class="panel-icon novavps-icon"></span>
|
||||
<h3>Novavps</h3>
|
||||
</div>
|
||||
<div class="status-badge" id="novavps-status">ожиданіе</div>
|
||||
<div class="status-badge" id="novavps-status">ожидание</div>
|
||||
<div class="status-detail" id="novavps-detail"></div>
|
||||
<div class="status-time" id="novavps-time"></div>
|
||||
</div>
|
||||
@@ -82,7 +82,7 @@
|
||||
<span class="panel-icon xray-icon"></span>
|
||||
<h3>Xray 1</h3>
|
||||
</div>
|
||||
<div class="status-badge" id="xray1-status">ожиданіе</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>
|
||||
@@ -91,7 +91,7 @@
|
||||
<span class="panel-icon xray-icon"></span>
|
||||
<h3>Xray 2</h3>
|
||||
</div>
|
||||
<div class="status-badge" id="xray2-status">ожиданіе</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>
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
<section class="card data-section" id="novavps-data-section">
|
||||
<div class="data-section-header" onclick="toggleSection('novavps-data')">
|
||||
<h2>Подключенія Novavps</h2>
|
||||
<h2>Подключения Novavps</h2>
|
||||
<span class="data-count" id="novavps-data-count"></span>
|
||||
<span class="collapse-arrow" id="novavps-data-arrow">▼</span>
|
||||
</div>
|
||||
@@ -110,9 +110,9 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Имя</th>
|
||||
<th>Протоколъ</th>
|
||||
<th>Адресъ</th>
|
||||
<th>Портъ</th>
|
||||
<th>Протокол</th>
|
||||
<th>Адрес</th>
|
||||
<th>Порт</th>
|
||||
<th>Безопасность</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -124,7 +124,7 @@
|
||||
|
||||
<section class="card data-section" id="xray1-data-section">
|
||||
<div class="data-section-header" onclick="toggleSection('xray1-data')">
|
||||
<h2>Исходящія Xray 1</h2>
|
||||
<h2>Исходящие Xray 1</h2>
|
||||
<span class="data-count" id="xray1-data-count"></span>
|
||||
<span class="collapse-arrow" id="xray1-data-arrow">▼</span>
|
||||
</div>
|
||||
@@ -133,10 +133,11 @@
|
||||
<table class="data-table" id="xray1-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Мѣтка</th>
|
||||
<th>Протоколъ</th>
|
||||
<th>Адресъ</th>
|
||||
<th>Портъ</th>
|
||||
<th>Метка</th>
|
||||
<th>Протокол</th>
|
||||
<th>Адрес</th>
|
||||
<th>Порт</th>
|
||||
<th>Задержка</th>
|
||||
<th>Синхр.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -148,7 +149,7 @@
|
||||
|
||||
<section class="card data-section hidden" id="xray2-data-section">
|
||||
<div class="data-section-header" onclick="toggleSection('xray2-data')">
|
||||
<h2>Исходящія Xray 2</h2>
|
||||
<h2>Исходящие Xray 2</h2>
|
||||
<span class="data-count" id="xray2-data-count"></span>
|
||||
<span class="collapse-arrow" id="xray2-data-arrow">▼</span>
|
||||
</div>
|
||||
@@ -157,10 +158,10 @@
|
||||
<table class="data-table" id="xray2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Мѣтка</th>
|
||||
<th>Протоколъ</th>
|
||||
<th>Адресъ</th>
|
||||
<th>Портъ</th>
|
||||
<th>Метка</th>
|
||||
<th>Протокол</th>
|
||||
<th>Адрес</th>
|
||||
<th>Порт</th>
|
||||
<th>Синхр.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -171,7 +172,7 @@
|
||||
</section>
|
||||
|
||||
<section class="last-run-section">
|
||||
<span class="last-run-label">Послѣдній запускъ:</span>
|
||||
<span class="last-run-label">Последний запуск:</span>
|
||||
<span id="last-run" class="last-run-value">Никогда</span>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -617,6 +617,28 @@ body::before {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Latency */
|
||||
.latency-cell {
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.latency-good {
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.latency-fair {
|
||||
color: var(--accent-amber);
|
||||
}
|
||||
|
||||
.latency-poor {
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
.latency-error {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Last run */
|
||||
.last-run-section {
|
||||
display: flex;
|
||||
|
||||
@@ -8,12 +8,23 @@ export interface AppConfig {
|
||||
novavpsUrl: string;
|
||||
}
|
||||
|
||||
export interface PingResult {
|
||||
tag: string;
|
||||
name: string;
|
||||
address: string;
|
||||
port: string;
|
||||
latency: number | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface PanelStatus {
|
||||
status: "idle" | "success" | "error";
|
||||
count?: number;
|
||||
outboundsCount?: number;
|
||||
synced?: boolean;
|
||||
syncedOutbounds?: string[];
|
||||
pingResults?: PingResult[];
|
||||
allPingsOk?: boolean;
|
||||
timestamp: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
@@ -39,7 +50,7 @@ const defaultState: AppState = {
|
||||
status: "idle",
|
||||
currentStep: "",
|
||||
novavps: { status: "idle", count: 0, timestamp: null, error: null },
|
||||
xray1: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null },
|
||||
xray1: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], pingResults: [], allPingsOk: false, timestamp: null, error: null },
|
||||
xray2: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null },
|
||||
};
|
||||
|
||||
@@ -90,7 +101,7 @@ export async function resetState(): Promise<AppState> {
|
||||
status: "running",
|
||||
currentStep: "initializing",
|
||||
novavps: { status: "idle", count: 0, timestamp: null, error: null },
|
||||
xray1: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null },
|
||||
xray1: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], pingResults: [], allPingsOk: false, timestamp: null, error: null },
|
||||
xray2: { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null },
|
||||
};
|
||||
await saveState(state);
|
||||
|
||||
37
src/ping.ts
Normal file
37
src/ping.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import net from "net";
|
||||
|
||||
export interface PingResult {
|
||||
tag: string;
|
||||
name: string;
|
||||
address: string;
|
||||
port: string;
|
||||
latency: number | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function tcpPing(host: string, port: number, timeout = 5000): Promise<number | null> {
|
||||
return new Promise((resolve) => {
|
||||
const start = Date.now();
|
||||
const socket = new net.Socket();
|
||||
|
||||
socket.setTimeout(timeout);
|
||||
|
||||
socket.on("connect", () => {
|
||||
const latency = Date.now() - start;
|
||||
socket.destroy();
|
||||
resolve(latency);
|
||||
});
|
||||
|
||||
socket.on("timeout", () => {
|
||||
socket.destroy();
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
socket.on("error", () => {
|
||||
socket.destroy();
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
socket.connect(port, host);
|
||||
});
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import fs from "fs/promises";
|
||||
import { parseNovavps, getValidConnections } from "./novavps";
|
||||
import { parseXrayPanel, syncNovavpsToXray } from "./xray-panel";
|
||||
import { closeBrowser } from "./browser";
|
||||
import { tcpPing } from "./ping";
|
||||
import {
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
@@ -107,6 +108,7 @@ async function runParser(): Promise<void> {
|
||||
isRunning = true;
|
||||
|
||||
const state = await resetState();
|
||||
const existingNovavpsPath = path.resolve(__dirname, "..", "data", "novavps-connections.json");
|
||||
|
||||
try {
|
||||
const config = await loadConfig(process.env.NOVAVPS_URL || "");
|
||||
@@ -133,11 +135,23 @@ async function runParser(): Promise<void> {
|
||||
let novavpsConnections: Awaited<ReturnType<typeof parseNovavps>> = [];
|
||||
try {
|
||||
novavpsConnections = await parseNovavps(novavpsUrl);
|
||||
|
||||
if (novavpsConnections.length === 0) {
|
||||
try {
|
||||
const existingRaw = await fs.readFile(existingNovavpsPath, "utf-8");
|
||||
const existing = JSON.parse(existingRaw);
|
||||
if (Array.isArray(existing) && existing.length > 0) {
|
||||
console.log(`[novavps] Страница недоступна, сохранено ${existing.length} предыдущих подключений`);
|
||||
novavpsConnections = existing;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
state.novavps = {
|
||||
status: "success",
|
||||
status: novavpsConnections.length > 0 ? "success" : "error",
|
||||
count: novavpsConnections.length,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: null,
|
||||
error: novavpsConnections.length === 0 ? "Нет данных" : null,
|
||||
};
|
||||
} catch (err) {
|
||||
state.novavps = {
|
||||
@@ -163,15 +177,18 @@ async function runParser(): Promise<void> {
|
||||
outboundsCount: xray.length,
|
||||
synced: false,
|
||||
syncedOutbounds: [],
|
||||
pingResults: panelStateKey === "xray1" ? [] : undefined,
|
||||
allPingsOk: panelStateKey === "xray1" ? false : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: null,
|
||||
};
|
||||
} catch (err) {
|
||||
const errorState = {
|
||||
status: "error" as const,
|
||||
const errorState: import("./data-store").PanelStatus = {
|
||||
status: "error",
|
||||
outboundsCount: 0,
|
||||
synced: false,
|
||||
syncedOutbounds: [],
|
||||
allPingsOk: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
@@ -193,6 +210,29 @@ async function runParser(): Promise<void> {
|
||||
const panelStateKey = panel.label as "xray1" | "xray2";
|
||||
state[panelStateKey].synced = true;
|
||||
state[panelStateKey].syncedOutbounds = syncedTags;
|
||||
|
||||
if (panel.label === "xray1") {
|
||||
state.currentStep = "pinging";
|
||||
await saveState(state);
|
||||
|
||||
const pingResults = [];
|
||||
for (let i = 0; i < valid.length; i++) {
|
||||
const conn = valid[i];
|
||||
const tag = `novavps${i + 1}`;
|
||||
const port = parseInt(conn.port) || 443;
|
||||
const latency = await tcpPing(conn.address, port);
|
||||
pingResults.push({
|
||||
tag,
|
||||
name: conn.name,
|
||||
address: conn.address,
|
||||
port: conn.port || String(port),
|
||||
latency,
|
||||
error: latency === null ? "Таймаут или ошибка" : null,
|
||||
});
|
||||
}
|
||||
state.xray1.pingResults = pingResults;
|
||||
state.xray1.allPingsOk = pingResults.every((p) => p.latency !== null);
|
||||
}
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
if (panel.label === "xray1") {
|
||||
@@ -203,6 +243,31 @@ async function runParser(): Promise<void> {
|
||||
}
|
||||
await saveState(state);
|
||||
}
|
||||
|
||||
if (panel.label === "xray2" && valid.length > 0 && state.xray1?.allPingsOk === true) {
|
||||
state.currentStep = "xray2_syncing";
|
||||
await saveState(state);
|
||||
|
||||
try {
|
||||
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
|
||||
state.xray2.synced = true;
|
||||
state.xray2.syncedOutbounds = valid.map((_, idx) => `novavps${idx + 1}`);
|
||||
} catch (err) {
|
||||
state.xray2.error = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
await saveState(state);
|
||||
} else if (panel.label === "xray2" && valid.length > 0) {
|
||||
console.log("[xray2] Пропускаю синхронизацию: не все пинги успешны");
|
||||
state.xray2 = {
|
||||
status: "error",
|
||||
outboundsCount: state.xray2.outboundsCount || 0,
|
||||
synced: false,
|
||||
syncedOutbounds: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
error: "Пинг не пройден — синхронизация пропущена",
|
||||
};
|
||||
await saveState(state);
|
||||
}
|
||||
}
|
||||
|
||||
state.status = "completed";
|
||||
@@ -300,6 +365,15 @@ app.get("/api/data/xray", authMiddleware, async (_req: Request, res: Response) =
|
||||
(async () => {
|
||||
const config = await loadConfig(process.env.NOVAVPS_URL || "");
|
||||
|
||||
// Сброс stale-состояния "running" после перезапуска сервера
|
||||
const state = await loadState();
|
||||
if (state.status === "running") {
|
||||
state.status = "idle";
|
||||
state.currentStep = "";
|
||||
await saveState(state);
|
||||
console.log("[server] Сброшено состояние 'running' после перезапуска");
|
||||
}
|
||||
|
||||
app.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`[server] Web interface running on http://0.0.0.0:${PORT}`);
|
||||
console.log(`[server] NOVAVPS_URL: ${config.novavpsUrl || "(not set)"}`);
|
||||
|
||||
@@ -316,6 +316,9 @@ export async function syncNovavpsToXray(
|
||||
}
|
||||
|
||||
const outbounds = (config.outbounds as Record<string, unknown>[]) || [];
|
||||
console.log(`[${panelLabel}] Всего outbound'ов в конфиге ДО: ${outbounds.length}`);
|
||||
const existingNovavpsTags = outbounds.filter((o) => String(o.tag || "").startsWith("novavps"));
|
||||
console.log(`[${panelLabel}] Из них с тегом novavps*: ${existingNovavpsTags.length}`);
|
||||
|
||||
connections.forEach((conn, idx) => {
|
||||
const tag = `novavps${idx + 1}`;
|
||||
@@ -324,15 +327,17 @@ export async function syncNovavpsToXray(
|
||||
const existingIdx = outbounds.findIndex((o) => o.tag === tag);
|
||||
if (existingIdx >= 0) {
|
||||
outbounds[existingIdx] = newOutbound;
|
||||
console.log(`[${panelLabel}] Outbound ${tag} обновлён`);
|
||||
console.log(`[${panelLabel}] Outbound ${tag} обновлён (${conn.address})`);
|
||||
} else {
|
||||
outbounds.push(newOutbound);
|
||||
console.log(`[${panelLabel}] Outbound ${tag} добавлен`);
|
||||
console.log(`[${panelLabel}] Outbound ${tag} добавлен (${conn.address})`);
|
||||
}
|
||||
});
|
||||
|
||||
config.outbounds = outbounds;
|
||||
const modifiedJson = JSON.stringify(config, null, 2);
|
||||
const novavpsCountAfter = (outbounds.filter((o) => String(o.tag || "").startsWith("novavps"))).length;
|
||||
console.log(`[${panelLabel}] После модификации: всего outbound'ов ${outbounds.length}, novavps*: ${novavpsCountAfter}`);
|
||||
|
||||
await page.evaluate((json) => {
|
||||
const cm = document.querySelector(".CodeMirror") as unknown as { CodeMirror?: { setValue: (v: string) => void } };
|
||||
@@ -350,7 +355,7 @@ export async function syncNovavpsToXray(
|
||||
});
|
||||
}, modifiedJson);
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const saved = await page.evaluate(() => {
|
||||
let clicked = false;
|
||||
@@ -366,13 +371,41 @@ export async function syncNovavpsToXray(
|
||||
});
|
||||
|
||||
if (saved) {
|
||||
console.log(`[${panelLabel}] Конфиг сохранён`);
|
||||
await page.waitForTimeout(2000);
|
||||
console.log(`[${panelLabel}] Кнопка 'Сохранить' нажата, жду сохранения...`);
|
||||
await page.waitForTimeout(3000);
|
||||
await page.waitForLoadState("networkidle");
|
||||
} else {
|
||||
console.log(`[${panelLabel}] Кнопка 'Сохранить' не найдена`);
|
||||
}
|
||||
|
||||
// Верификация — читаем JSON из редактора и проверяем novavps-теги
|
||||
console.log(`[${panelLabel}] Проверка сохранённого JSON...`);
|
||||
const verifyJson = await page.evaluate(() => {
|
||||
const cm = document.querySelector(".CodeMirror") as unknown as { CodeMirror?: { getValue: () => string } };
|
||||
if (cm?.CodeMirror) return cm.CodeMirror.getValue();
|
||||
const ta = document.querySelector("textarea");
|
||||
return ta?.value || "";
|
||||
});
|
||||
|
||||
if (verifyJson) {
|
||||
try {
|
||||
const verifyConfig = JSON.parse(verifyJson);
|
||||
const verifyOutbounds = (verifyConfig.outbounds as Record<string, unknown>[]) || [];
|
||||
const verifyNovavps = verifyOutbounds.filter((o) => String(o.tag || "").startsWith("novavps"));
|
||||
console.log(`[${panelLabel}] После сохранения: outbound'ов ${verifyOutbounds.length}, novavps*: ${verifyNovavps.length}`);
|
||||
verifyNovavps.forEach((o) => {
|
||||
const settings = (o as Record<string, unknown>).settings as Record<string, unknown> | undefined;
|
||||
const vnext = settings?.vnext as Record<string, unknown>[] | undefined;
|
||||
const addr = vnext?.[0]?.address as string | undefined;
|
||||
console.log(`[${panelLabel}] ${o.tag}: ${addr || "N/A"}`);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(`[${panelLabel}] Ошибка парсинга JSON при верификации: ${e}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[${panelLabel}] Не удалось прочитать JSON после сохранения`);
|
||||
}
|
||||
|
||||
await page.goto(`${baseUrl}/panel/`, { waitUntil: "networkidle", timeout: 30000 });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user