v2
This commit is contained in:
@@ -9,6 +9,7 @@ const novavpsUrlInput = $("#novavps-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");
|
||||
@@ -92,7 +93,9 @@ 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;
|
||||
|
||||
@@ -317,6 +320,22 @@ 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) {
|
||||
|
||||
@@ -54,6 +54,10 @@
|
||||
<span class="btn-icon">⟳</span>
|
||||
Обновить сейчас
|
||||
</button>
|
||||
<button id="sync-xray2-btn" class="btn btn-secondary btn-lg">
|
||||
<span class="btn-icon">↻</span>
|
||||
Синхр. Xray 2
|
||||
</button>
|
||||
<div id="run-msg" class="msg hidden"></div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -201,6 +201,24 @@ body::before {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--accent-blue);
|
||||
border: 1px solid var(--accent-blue);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
box-shadow: 0 0 15px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
filter: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
|
||||
167
src/server.ts
167
src/server.ts
@@ -7,7 +7,7 @@ import crypto from "crypto";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
|
||||
import { parseNovavps, getValidConnections } from "./novavps";
|
||||
import { parseNovavps, getValidConnections, NovavpsConnection } from "./novavps";
|
||||
import { parseXrayPanel, syncNovavpsToXray } from "./xray-panel";
|
||||
import { closeBrowser } from "./browser";
|
||||
import { tcpPing } from "./ping";
|
||||
@@ -204,46 +204,52 @@ async function runParser(): Promise<void> {
|
||||
state.currentStep = `${panel.label}_syncing`;
|
||||
await saveState(state);
|
||||
|
||||
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;
|
||||
// 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;
|
||||
|
||||
if (panel.label === "xray1") {
|
||||
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,
|
||||
});
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
await saveState(state);
|
||||
}
|
||||
|
||||
// Синхронизация xray2 — только если все пинги xray1 успешны
|
||||
if (panel.label === "xray2" && valid.length > 0 && state.xray1?.allPingsOk === true) {
|
||||
state.currentStep = "xray2_syncing";
|
||||
await saveState(state);
|
||||
@@ -258,15 +264,6 @@ async function runParser(): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +283,92 @@ async function runParser(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
let isXray2Running = false;
|
||||
|
||||
async function runXray2Sync(): Promise<void> {
|
||||
if (isXray2Running) return;
|
||||
isXray2Running = true;
|
||||
|
||||
const state = await loadState();
|
||||
state.currentStep = "xray2_syncing";
|
||||
state.xray2.status = "idle";
|
||||
state.xray2.synced = false;
|
||||
state.xray2.error = null;
|
||||
await saveState(state);
|
||||
|
||||
const existingNovavpsPath = path.resolve(__dirname, "..", "data", "novavps-connections.json");
|
||||
|
||||
try {
|
||||
let connections: NovavpsConnection[] = [];
|
||||
try {
|
||||
const raw = await fs.readFile(existingNovavpsPath, "utf-8");
|
||||
connections = JSON.parse(raw);
|
||||
} catch {
|
||||
state.xray2 = {
|
||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||
timestamp: new Date().toISOString(), error: "Нет сохранённых подключений Novavps",
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const valid = getValidConnections(connections);
|
||||
if (valid.length === 0) {
|
||||
state.xray2 = {
|
||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||
timestamp: new Date().toISOString(), error: "Нет валидных подключений",
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = getPanels().find((p) => p.label === "xray2");
|
||||
if (!panel) {
|
||||
state.xray2 = {
|
||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||
timestamp: new Date().toISOString(), error: "Xray2 панель не настроена",
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
await syncNovavpsToXray(valid, panel.url, panel.username, panel.password, "xray2");
|
||||
|
||||
state.xray2 = {
|
||||
status: "success",
|
||||
outboundsCount: (await parseXrayPanel(panel.url, panel.username, panel.password)).length,
|
||||
synced: true,
|
||||
syncedOutbounds: valid.map((_, idx) => `novavps${idx + 1}`),
|
||||
timestamp: new Date().toISOString(),
|
||||
error: null,
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
} catch (err) {
|
||||
state.xray2 = {
|
||||
status: "error", outboundsCount: 0, synced: false, syncedOutbounds: [],
|
||||
timestamp: new Date().toISOString(), error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
state.currentStep = "done";
|
||||
await saveState(state);
|
||||
} finally {
|
||||
await closeBrowser();
|
||||
isXray2Running = false;
|
||||
}
|
||||
}
|
||||
|
||||
app.post("/api/sync-xray2", authMiddleware, async (_req: Request, res: Response) => {
|
||||
if (isXray2Running) {
|
||||
res.status(409).json({ error: "Синхронизация Xray2 уже запущена" });
|
||||
return;
|
||||
}
|
||||
runXray2Sync().catch((err) => console.error("[server] Xray2 sync error:", err));
|
||||
res.json({ ok: true, message: "Синхронизация Xray2 запущена" });
|
||||
});
|
||||
|
||||
app.post("/api/login", async (req: Request, res: Response) => {
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) {
|
||||
|
||||
Reference in New Issue
Block a user