v3
This commit is contained in:
@@ -4,7 +4,7 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
- TZ=Asia/Yekaterinburg
|
||||
- PARSE_INTERVAL=3600
|
||||
- PARSE_INTERVAL=1800
|
||||
|
||||
# ---------- Парсер novavps ----------
|
||||
- NOVAVPS_URL=
|
||||
|
||||
40
package-lock.json
generated
40
package-lock.json
generated
@@ -11,7 +11,8 @@
|
||||
"cookie-parser": "^1.4.7",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^5.2.1",
|
||||
"playwright": "^1.51.0"
|
||||
"playwright": "^1.51.0",
|
||||
"prom-client": "^15.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
@@ -62,6 +63,15 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
|
||||
"integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||
@@ -244,6 +254,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bintrees": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
|
||||
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||
@@ -888,6 +904,19 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/prom-client": {
|
||||
"version": "15.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz",
|
||||
"integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"tdigest": "^0.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16 || ^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -1094,6 +1123,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/tdigest": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
|
||||
"integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bintrees": "1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"cookie-parser": "^1.4.7",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^5.2.1",
|
||||
"playwright": "^1.51.0"
|
||||
"playwright": "^1.51.0",
|
||||
"prom-client": "^15.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
|
||||
@@ -45,9 +45,10 @@ const stepLabels = {
|
||||
novavps_fetching: "Получение данных NOVAVPS...",
|
||||
xray1_parsing: "Чтение панели Xray 1...",
|
||||
xray1_syncing: "Синхронизация Xray 1...",
|
||||
pinging: "Проверка соединений...",
|
||||
pinging: "Проверка соединений (Xray 1)...",
|
||||
xray2_parsing: "Чтение панели Xray 2...",
|
||||
xray2_syncing: "Синхронизация Xray 2...",
|
||||
xray2_pinging: "Проверка соединений (Xray 2)...",
|
||||
done: "Завершено",
|
||||
novavps_url_missing: "Ошибка: ссылка NOVAVPS не задана",
|
||||
no_xray_panels: "Ошибка: нет настроенных панелей Xray",
|
||||
@@ -99,6 +100,9 @@ async function fetchStatus() {
|
||||
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);
|
||||
|
||||
@@ -72,15 +72,6 @@
|
||||
<section class="status-section">
|
||||
<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-detail" id="novavps-detail"></div>
|
||||
<div class="status-time" id="novavps-time"></div>
|
||||
</div>
|
||||
<div class="status-card" id="xray1-card">
|
||||
<div class="status-card-header">
|
||||
<span class="panel-icon xray-icon"></span>
|
||||
@@ -99,29 +90,14 @@
|
||||
<div class="status-detail" id="xray2-detail"></div>
|
||||
<div class="status-time" id="xray2-time"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card data-section" id="novavps-data-section">
|
||||
<div class="data-section-header" onclick="toggleSection('novavps-data')">
|
||||
<h2>Подключения Novavps</h2>
|
||||
<span class="data-count" id="novavps-data-count"></span>
|
||||
<span class="collapse-arrow" id="novavps-data-arrow">▼</span>
|
||||
</div>
|
||||
<div class="data-content" id="novavps-data">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table" id="novavps-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Имя</th>
|
||||
<th>Протокол</th>
|
||||
<th>Адрес</th>
|
||||
<th>Порт</th>
|
||||
<th>Безопасность</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="novavps-tbody"></tbody>
|
||||
</table>
|
||||
<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-detail" id="novavps-detail"></div>
|
||||
<div class="status-time" id="novavps-time"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -175,6 +151,30 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card data-section" id="novavps-data-section">
|
||||
<div class="data-section-header" onclick="toggleSection('novavps-data')">
|
||||
<h2>Подключения Novavps</h2>
|
||||
<span class="data-count" id="novavps-data-count"></span>
|
||||
<span class="collapse-arrow" id="novavps-data-arrow">▼</span>
|
||||
</div>
|
||||
<div class="data-content" id="novavps-data">
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table" id="novavps-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Имя</th>
|
||||
<th>Протокол</th>
|
||||
<th>Адрес</th>
|
||||
<th>Порт</th>
|
||||
<th>Безопасность</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="novavps-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="last-run-section">
|
||||
<span class="last-run-label">Последний запуск:</span>
|
||||
<span id="last-run" class="last-run-value">Никогда</span>
|
||||
|
||||
280
src/server.ts
280
src/server.ts
@@ -7,6 +7,7 @@ import crypto from "crypto";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
|
||||
import promClient from 'prom-client';
|
||||
import { parseNovavps, getValidConnections, NovavpsConnection } from "./novavps";
|
||||
import { parseXrayPanel, syncNovavpsToXray } from "./xray-panel";
|
||||
import { closeBrowser } from "./browser";
|
||||
@@ -20,6 +21,32 @@ import {
|
||||
AppState,
|
||||
} from "./data-store";
|
||||
|
||||
// Prometheus Metrics Initialization
|
||||
const prometheusRegistry = new promClient.Registry();
|
||||
|
||||
const parserRunsTotal = new promClient.Counter({
|
||||
name: 'vpn_parser_runs_total',
|
||||
help: 'Total number of times the Novavps parser has been run.',
|
||||
labelNames: ['status'],
|
||||
});
|
||||
|
||||
const xray2SyncRunsTotal = new promClient.Counter({
|
||||
name: 'xray2_sync_runs_total',
|
||||
help: 'Total attempts to synchronize Xray2 panel settings.',
|
||||
labelNames: ['status'],
|
||||
});
|
||||
|
||||
const novavpsConnectionsCount = new promClient.Gauge({
|
||||
name: 'novavps_connections_count',
|
||||
help: 'Current number of valid Novavps connections loaded.',
|
||||
});
|
||||
|
||||
const panelStatus = new promClient.Gauge({
|
||||
name: 'panel_status',
|
||||
help: 'Operational status of a panel (1=OK, 0=Error/Offline). Labels: label',
|
||||
labelNames: ['label'],
|
||||
});
|
||||
|
||||
const app = express();
|
||||
const PORT = parseInt(process.env.PORT || "3000", 10);
|
||||
|
||||
@@ -107,6 +134,10 @@ async function runParser(): Promise<void> {
|
||||
if (isRunning) return;
|
||||
isRunning = true;
|
||||
|
||||
novavpsConnectionsCount.set(0);
|
||||
panelStatus.reset();
|
||||
parserRunsTotal.inc({ status: 'started' });
|
||||
|
||||
const state = await resetState();
|
||||
const existingNovavpsPath = path.resolve(__dirname, "..", "data", "novavps-connections.json");
|
||||
|
||||
@@ -117,7 +148,9 @@ async function runParser(): Promise<void> {
|
||||
if (!novavpsUrl) {
|
||||
state.status = "error";
|
||||
state.currentStep = "novavps_url_missing";
|
||||
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Novavps URL не настроен" };
|
||||
await saveState(state);
|
||||
parserRunsTotal.inc({ status: 'failed' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -125,7 +158,9 @@ async function runParser(): Promise<void> {
|
||||
if (panels.length === 0) {
|
||||
state.status = "error";
|
||||
state.currentStep = "no_xray_panels";
|
||||
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: "Не настроены панели Xray" };
|
||||
await saveState(state);
|
||||
parserRunsTotal.inc({ status: 'failed' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -144,7 +179,9 @@ async function runParser(): Promise<void> {
|
||||
console.log(`[novavps] Страница недоступна, сохранено ${existing.length} предыдущих подключений`);
|
||||
novavpsConnections = existing;
|
||||
}
|
||||
} catch {}
|
||||
} catch {
|
||||
console.warn("[novavps] Не удалось прочитать сохранённые подключения");
|
||||
}
|
||||
}
|
||||
|
||||
state.novavps = {
|
||||
@@ -154,14 +191,17 @@ async function runParser(): Promise<void> {
|
||||
error: novavpsConnections.length === 0 ? "Нет данных" : null,
|
||||
};
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
console.error("[novavps] Ошибка парсинга:", errMsg);
|
||||
state.novavps = {
|
||||
status: "error",
|
||||
count: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
await saveState(state);
|
||||
novavpsConnectionsCount.set(novavpsConnections.length);
|
||||
|
||||
const valid = getValidConnections(novavpsConnections);
|
||||
|
||||
@@ -170,11 +210,11 @@ async function runParser(): Promise<void> {
|
||||
await saveState(state);
|
||||
|
||||
try {
|
||||
const xray = await parseXrayPanel(panel.url, panel.username, panel.password);
|
||||
await parseXrayPanel(panel.url, panel.username, panel.password);
|
||||
const panelStateKey = panel.label as "xray1" | "xray2";
|
||||
state[panelStateKey] = {
|
||||
status: "success",
|
||||
outboundsCount: xray.length,
|
||||
outboundsCount: 0,
|
||||
synced: false,
|
||||
syncedOutbounds: [],
|
||||
pingResults: panelStateKey === "xray1" ? [] : undefined,
|
||||
@@ -182,7 +222,10 @@ async function runParser(): Promise<void> {
|
||||
timestamp: new Date().toISOString(),
|
||||
error: null,
|
||||
};
|
||||
panelStatus.set({ label: panel.label }, 1);
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[${panel.label}] Ошибка парсинга:`, errMsg);
|
||||
const errorState: import("./data-store").PanelStatus = {
|
||||
status: "error",
|
||||
outboundsCount: 0,
|
||||
@@ -190,93 +233,153 @@ async function runParser(): Promise<void> {
|
||||
syncedOutbounds: [],
|
||||
allPingsOk: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: errMsg,
|
||||
};
|
||||
if (panel.label === "xray1") {
|
||||
state.xray1 = errorState;
|
||||
} else {
|
||||
state.xray2 = errorState;
|
||||
}
|
||||
panelStatus.set({ label: panel.label }, 0);
|
||||
}
|
||||
await saveState(state);
|
||||
|
||||
if (valid.length > 0) {
|
||||
state.currentStep = `${panel.label}_syncing`;
|
||||
await saveState(state);
|
||||
if (valid.length === 0) continue;
|
||||
const panelStateKey = panel.label as "xray1" | "xray2";
|
||||
|
||||
// xray2 синхронизируется ниже, при allPingsOk от xray1
|
||||
if (panel.label === "xray2") {
|
||||
console.log("[xray2] Ожидаю пинги xray1 для синхронизации");
|
||||
} else {
|
||||
try {
|
||||
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
|
||||
const syncedTags = valid.map((_, idx) => `novavps${idx + 1}`);
|
||||
const panelStateKey = panel.label as "xray1" | "xray2";
|
||||
state[panelStateKey].synced = true;
|
||||
state[panelStateKey].syncedOutbounds = syncedTags;
|
||||
// --- XRAY 1: ping + sync (все + novavps0) одним вызовом ---
|
||||
if (panel.label === "xray1") {
|
||||
try {
|
||||
state.currentStep = "pinging";
|
||||
await saveState(state);
|
||||
|
||||
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") {
|
||||
state.xray1.error = errMsg;
|
||||
} else {
|
||||
state.xray2.error = errMsg;
|
||||
}
|
||||
const pingResults: Array<{
|
||||
tag: string; name: string; address: string; port: string;
|
||||
latency: number | null; error: string | null;
|
||||
}> = [];
|
||||
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);
|
||||
|
||||
// Найти соединение с лучшим пингом
|
||||
let bestPingIndex = -1;
|
||||
let bestLatency = Infinity;
|
||||
pingResults.forEach((p, i) => {
|
||||
if (p.latency !== null && p.latency < bestLatency) {
|
||||
bestLatency = p.latency;
|
||||
bestPingIndex = i;
|
||||
}
|
||||
});
|
||||
|
||||
// Собрать массив: novavps0 (best) + novavps1..N
|
||||
let syncConnections = [...valid];
|
||||
let syncedTags = valid.map((_, idx) => `novavps${idx + 1}`);
|
||||
if (bestPingIndex >= 0) {
|
||||
syncConnections = [valid[bestPingIndex], ...valid];
|
||||
syncedTags = ["novavps0", ...syncedTags];
|
||||
}
|
||||
|
||||
state.currentStep = "xray1_syncing";
|
||||
await saveState(state);
|
||||
|
||||
await syncNovavpsToXray(syncConnections, panel.url, panel.username, panel.password, panel.label, 0);
|
||||
state.xray1.synced = true;
|
||||
state.xray1.outboundsCount = syncedTags.length;
|
||||
state.xray1.syncedOutbounds = syncedTags;
|
||||
panelStatus.set({ label: "xray1" }, 1);
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
console.error("[xray1] Ошибка синхронизации:", errMsg);
|
||||
state.xray1.error = errMsg;
|
||||
panelStatus.set({ label: "xray1" }, 0);
|
||||
}
|
||||
await saveState(state);
|
||||
}
|
||||
|
||||
// Синхронизация xray2 — только если все пинги xray1 успешны
|
||||
if (panel.label === "xray2" && valid.length > 0 && state.xray1?.allPingsOk === true) {
|
||||
state.currentStep = "xray2_syncing";
|
||||
await saveState(state);
|
||||
|
||||
// --- XRAY 2: ping + sync (все + novavps0) одним вызовом ---
|
||||
if (panel.label === "xray2") {
|
||||
try {
|
||||
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, panel.label);
|
||||
state.currentStep = "xray2_pinging";
|
||||
await saveState(state);
|
||||
|
||||
const pingResults: Array<{
|
||||
tag: string; name: string; address: string; port: string;
|
||||
latency: number | null; error: string | null;
|
||||
}> = [];
|
||||
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.xray2.pingResults = pingResults;
|
||||
state.xray2.allPingsOk = pingResults.every((p) => p.latency !== null);
|
||||
|
||||
// Найти соединение с лучшим пингом
|
||||
let bestPingIndex = -1;
|
||||
let bestLatency = Infinity;
|
||||
pingResults.forEach((p, i) => {
|
||||
if (p.latency !== null && p.latency < bestLatency) {
|
||||
bestLatency = p.latency;
|
||||
bestPingIndex = i;
|
||||
}
|
||||
});
|
||||
|
||||
// Собрать массив: novavps0 (best) + novavps1..N
|
||||
let syncConnections = [...valid];
|
||||
let syncedTags = valid.map((_, idx) => `novavps${idx + 1}`);
|
||||
if (bestPingIndex >= 0) {
|
||||
syncConnections = [valid[bestPingIndex], ...valid];
|
||||
syncedTags = ["novavps0", ...syncedTags];
|
||||
}
|
||||
|
||||
state.currentStep = "xray2_syncing";
|
||||
await saveState(state);
|
||||
|
||||
await syncNovavpsToXray(syncConnections, panel.url, panel.username, panel.password, panel.label, 0);
|
||||
state.xray2.synced = true;
|
||||
state.xray2.syncedOutbounds = valid.map((_, idx) => `novavps${idx + 1}`);
|
||||
state.xray2.outboundsCount = syncedTags.length;
|
||||
state.xray2.syncedOutbounds = syncedTags;
|
||||
panelStatus.set({ label: "xray2" }, 1);
|
||||
} catch (err) {
|
||||
state.xray2.error = err instanceof Error ? err.message : String(err);
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
console.error("[xray2] Ошибка синхронизации:", errMsg);
|
||||
state.xray2.error = errMsg;
|
||||
panelStatus.set({ label: "xray2" }, 0);
|
||||
}
|
||||
await saveState(state);
|
||||
} else if (panel.label === "xray2" && valid.length > 0) {
|
||||
console.log("[xray2] Пропускаю синхронизацию: не все пинги успешны");
|
||||
}
|
||||
}
|
||||
|
||||
state.status = "completed";
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
parserRunsTotal.inc({ status: 'success' });
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
console.error("[server] Критическая ошибка парсера:", errMsg);
|
||||
state.status = "error";
|
||||
state.currentStep = "fatal";
|
||||
if (state.novavps.status === "idle") {
|
||||
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: err instanceof Error ? err.message : String(err) };
|
||||
state.novavps = { status: "error", count: 0, timestamp: new Date().toISOString(), error: errMsg };
|
||||
} else {
|
||||
state.novavps.error = (state.novavps.error || "") + " | " + errMsg;
|
||||
}
|
||||
await saveState(state);
|
||||
parserRunsTotal.inc({ status: 'failed' });
|
||||
} finally {
|
||||
await closeBrowser();
|
||||
isRunning = false;
|
||||
@@ -289,6 +392,9 @@ async function runXray2Sync(): Promise<void> {
|
||||
if (isXray2Running) return;
|
||||
isXray2Running = true;
|
||||
|
||||
xray2SyncRunsTotal.inc({ status: 'started' });
|
||||
panelStatus.set({ label: "xray2" }, 0);
|
||||
|
||||
const state = await loadState();
|
||||
state.currentStep = "xray2_syncing";
|
||||
state.xray2.status = "idle";
|
||||
@@ -304,34 +410,43 @@ async function runXray2Sync(): Promise<void> {
|
||||
const raw = await fs.readFile(existingNovavpsPath, "utf-8");
|
||||
connections = JSON.parse(raw);
|
||||
} catch {
|
||||
console.warn("[xray2] Нет сохранённых подключений Novavps");
|
||||
state.xray2 = {
|
||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||
timestamp: new Date().toISOString(), error: "Нет сохранённых подключений Novavps",
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
xray2SyncRunsTotal.inc({ status: 'failed' });
|
||||
panelStatus.set({ label: "xray2" }, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const valid = getValidConnections(connections);
|
||||
if (valid.length === 0) {
|
||||
console.warn("[xray2] Нет валидных подключений Novavps");
|
||||
state.xray2 = {
|
||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||
timestamp: new Date().toISOString(), error: "Нет валидных подключений",
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
xray2SyncRunsTotal.inc({ status: 'failed' });
|
||||
panelStatus.set({ label: "xray2" }, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = getPanels().find((p) => p.label === "xray2");
|
||||
if (!panel) {
|
||||
console.warn("[xray2] Панель Xray2 не настроена в переменных окружения");
|
||||
state.xray2 = {
|
||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||
timestamp: new Date().toISOString(), error: "Xray2 панель не настроена",
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
xray2SyncRunsTotal.inc({ status: 'failed' });
|
||||
panelStatus.set({ label: "xray2" }, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -347,13 +462,19 @@ async function runXray2Sync(): Promise<void> {
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
xray2SyncRunsTotal.inc({ status: 'success' });
|
||||
panelStatus.set({ label: "xray2" }, 1);
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
console.error("[xray2] Критическая ошибка синхронизации:", errMsg);
|
||||
state.xray2 = {
|
||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||
timestamp: new Date().toISOString(), error: err instanceof Error ? err.message : String(err),
|
||||
timestamp: new Date().toISOString(), error: errMsg,
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
xray2SyncRunsTotal.inc({ status: 'failed' });
|
||||
panelStatus.set({ label: "xray2" }, 0);
|
||||
} finally {
|
||||
await closeBrowser();
|
||||
isXray2Running = false;
|
||||
@@ -365,6 +486,12 @@ app.post("/api/sync-xray2", authMiddleware, async (_req: Request, res: Response)
|
||||
res.status(409).json({ error: "Синхронизация Xray2 уже запущена" });
|
||||
return;
|
||||
}
|
||||
const panel = getPanels().find((p) => p.label === "xray2");
|
||||
if (!panel) {
|
||||
console.warn("[api/sync-xray2] Xray2 панель не настроена");
|
||||
res.status(400).json({ error: "Xray2 панель не настроена. Проверьте переменные XRAY_URL2, XRAY_USERNAME2, XRAY_PASSWORD2" });
|
||||
return;
|
||||
}
|
||||
runXray2Sync().catch((err) => console.error("[server] Xray2 sync error:", err));
|
||||
res.json({ ok: true, message: "Синхронизация Xray2 запущена" });
|
||||
});
|
||||
@@ -396,7 +523,10 @@ app.post("/api/logout", (_req: Request, res: Response) => {
|
||||
|
||||
app.get("/api/status", authMiddleware, async (_req: Request, res: Response) => {
|
||||
const state = await loadState();
|
||||
res.json(state);
|
||||
res.json({
|
||||
...state,
|
||||
configuredPanels: getPanels().map(p => p.label),
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/api/run", authMiddleware, async (_req: Request, res: Response) => {
|
||||
@@ -445,10 +575,20 @@ app.get("/api/data/xray", authMiddleware, async (_req: Request, res: Response) =
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/metrics", authMiddleware, async (_req: Request, res: Response) => {
|
||||
res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
|
||||
res.end(prometheusRegistry.metrics());
|
||||
});
|
||||
|
||||
// Global error handler
|
||||
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
console.error("[server] Unhandled error:", err);
|
||||
res.status(500).json({ error: "Внутренняя ошибка сервера" });
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const config = await loadConfig(process.env.NOVAVPS_URL || "");
|
||||
|
||||
// Сброс stale-состояния "running" после перезапуска сервера
|
||||
const state = await loadState();
|
||||
if (state.status === "running") {
|
||||
state.status = "idle";
|
||||
@@ -457,9 +597,27 @@ app.get("/api/data/xray", authMiddleware, async (_req: Request, res: Response) =
|
||||
console.log("[server] Сброшено состояние 'running' после перезапуска");
|
||||
}
|
||||
|
||||
if (state.xray2.status === "error" && !process.env.XRAY_URL2) {
|
||||
state.xray2 = { status: "idle", outboundsCount: 0, synced: false, syncedOutbounds: [], timestamp: null, error: null };
|
||||
await saveState(state);
|
||||
console.log("[server] Сброшена ошибка Xray2 — панель не настроена");
|
||||
}
|
||||
|
||||
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)"}`);
|
||||
console.log(`[server] Panels: ${getPanels().map((p) => p.label).join(", ") || "(none)"}`);
|
||||
});
|
||||
|
||||
const PARSE_INTERVAL_MS = (parseInt(process.env.PARSE_INTERVAL || "1800", 10)) * 1000;
|
||||
|
||||
setTimeout(() => {
|
||||
console.log(`[server] Автозапуск парсера через 5с...`);
|
||||
runParser();
|
||||
}, 5000);
|
||||
|
||||
setInterval(() => {
|
||||
console.log(`[server] Плановый запуск парсера (интервал ${PARSE_INTERVAL_MS / 1000}с)...`);
|
||||
runParser();
|
||||
}, PARSE_INTERVAL_MS);
|
||||
})();
|
||||
|
||||
@@ -98,8 +98,8 @@ function buildVlessOutbound(conn: NovavpsConnection, tag: string): Record<string
|
||||
}
|
||||
|
||||
async function loginAndNavigate(page: Page, url: string, username: string, password: string): Promise<void> {
|
||||
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.goto(url, { waitUntil: "load", timeout: 60000 });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const loginInput = await page.$('input[type="text"], input[name="username"], input[name="login"]');
|
||||
const passInput = await page.$('input[type="password"]');
|
||||
@@ -110,8 +110,7 @@ async function loginAndNavigate(page: Page, url: string, username: string, passw
|
||||
const submitBtn = await page.$('button[type="submit"], input[type="submit"]');
|
||||
if (submitBtn) await submitBtn.click();
|
||||
else await page.keyboard.press("Enter");
|
||||
await page.waitForTimeout(3000);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(5000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,8 +125,8 @@ export async function parseXrayPanel(
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.goto(url, { waitUntil: "load", timeout: 60000 });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const loginInput = await page.$('input[type="text"], input[name="username"], input[name="login"]');
|
||||
const passInput = await page.$('input[type="password"]');
|
||||
@@ -141,15 +140,14 @@ export async function parseXrayPanel(
|
||||
if (submitBtn) await submitBtn.click();
|
||||
else await page.keyboard.press("Enter");
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(5000);
|
||||
}
|
||||
|
||||
const baseUrl = new URL(url).origin;
|
||||
const xrayUrl = `${baseUrl}/panel/xray`;
|
||||
|
||||
await page.goto(xrayUrl, { waitUntil: "networkidle", timeout: 30000 });
|
||||
await page.waitForTimeout(3000);
|
||||
await page.goto(xrayUrl, { waitUntil: "load", timeout: 60000 });
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// Кликаем на вкладку "Исходящие подключения"
|
||||
await page.evaluate(() => {
|
||||
@@ -164,8 +162,7 @@ export async function parseXrayPanel(
|
||||
});
|
||||
});
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(4000);
|
||||
|
||||
const outbounds = await page.evaluate(() => {
|
||||
const rows: Array<{
|
||||
@@ -242,7 +239,8 @@ export async function syncNovavpsToXray(
|
||||
url: string,
|
||||
username: string,
|
||||
password: string,
|
||||
panelLabel = "xray"
|
||||
panelLabel = "xray",
|
||||
startIndex = 1
|
||||
): Promise<void> {
|
||||
if (connections.length === 0) {
|
||||
console.log(`[${panelLabel}] Нет подключений для синхронизации`);
|
||||
@@ -260,8 +258,8 @@ export async function syncNovavpsToXray(
|
||||
const baseUrl = new URL(url).origin;
|
||||
|
||||
// Переход на страницу настроек Xray
|
||||
await page.goto(`${baseUrl}/panel/xray`, { waitUntil: "networkidle", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.goto(`${baseUrl}/panel/xray`, { waitUntil: "load", timeout: 60000 });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Клик на вкладку "Расширенный шаблон"
|
||||
const templateClicked = await page.evaluate(() => {
|
||||
@@ -282,8 +280,7 @@ export async function syncNovavpsToXray(
|
||||
return;
|
||||
}
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(4000);
|
||||
|
||||
// Читаем JSON через CodeMirror API
|
||||
const configJson = await page.evaluate(() => {
|
||||
@@ -321,7 +318,7 @@ export async function syncNovavpsToXray(
|
||||
console.log(`[${panelLabel}] Из них с тегом novavps*: ${existingNovavpsTags.length}`);
|
||||
|
||||
connections.forEach((conn, idx) => {
|
||||
const tag = `novavps${idx + 1}`;
|
||||
const tag = `novavps${startIndex + idx}`;
|
||||
const newOutbound = buildVlessOutbound(conn, tag);
|
||||
|
||||
const existingIdx = outbounds.findIndex((o) => o.tag === tag);
|
||||
@@ -355,7 +352,7 @@ export async function syncNovavpsToXray(
|
||||
});
|
||||
}, modifiedJson);
|
||||
|
||||
await page.waitForTimeout(1500);
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const saved = await page.evaluate(() => {
|
||||
let clicked = false;
|
||||
@@ -372,8 +369,7 @@ export async function syncNovavpsToXray(
|
||||
|
||||
if (saved) {
|
||||
console.log(`[${panelLabel}] Кнопка 'Сохранить' нажата, жду сохранения...`);
|
||||
await page.waitForTimeout(3000);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(5000);
|
||||
} else {
|
||||
console.log(`[${panelLabel}] Кнопка 'Сохранить' не найдена`);
|
||||
}
|
||||
@@ -406,8 +402,8 @@ export async function syncNovavpsToXray(
|
||||
console.log(`[${panelLabel}] Не удалось прочитать JSON после сохранения`);
|
||||
}
|
||||
|
||||
await page.goto(`${baseUrl}/panel/`, { waitUntil: "networkidle", timeout: 30000 });
|
||||
await page.waitForTimeout(3000);
|
||||
await page.goto(`${baseUrl}/panel/`, { waitUntil: "load", timeout: 60000 });
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
const dashboardText = await page.evaluate(() => document.body.innerText);
|
||||
console.log(`[${panelLabel}] Текст дашборда (первые 500 символов):`);
|
||||
@@ -433,7 +429,7 @@ export async function syncNovavpsToXray(
|
||||
|
||||
if (restarted.startsWith("clicked")) {
|
||||
console.log(`[${panelLabel}] Нажата кнопка: ${restarted}`);
|
||||
await page.waitForTimeout(3000);
|
||||
await page.waitForTimeout(5000);
|
||||
} else {
|
||||
console.log(`[${panelLabel}] Кнопка не найдена (${restarted})`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user