From f1a587e00de775c6ee0b449a94f8eab8e7bc9e87 Mon Sep 17 00:00:00 2001 From: penguinehis Date: Mon, 11 May 2026 22:10:17 -0300 Subject: [PATCH] Mult node launch --- admin/assets/app.css | 36 +++++++++++ admin/assets/app.js | 142 +++++++++++++++++++++++++++++-------------- admin/index.html | 11 ++-- 3 files changed, 138 insertions(+), 51 deletions(-) diff --git a/admin/assets/app.css b/admin/assets/app.css index f248406..296153e 100644 --- a/admin/assets/app.css +++ b/admin/assets/app.css @@ -591,3 +591,39 @@ body.drawer-open .drawer-backdrop,body.sidebar-open .drawer-backdrop{display:blo .servers-status-grid{grid-template-columns:1fr;} .server-mini-grid{grid-template-columns:1fr;} } + + +/* --- Xray full config and select color fixes --- */ +.field select, +select, +#xrayServerSelect, +#wzLogLevel, +#wzProtocol, +#wzNetwork, +#wzXHTTPMode, +#wzTLS, +#wzSSMethod { + color:#f3f7ff !important; + background:#070b12 !important; + border-color:rgba(34,211,238,.26) !important; + color-scheme:dark; +} +.field select option, +select option, +.field select optgroup, +select optgroup { + color:#f3f7ff !important; + background:#0b111a !important; +} +#xrayServerHint.hidden, +#sshServerHint.hidden { display:none !important; } + +select option:checked, +.field select option:checked { + background:#1f2a3a !important; + color:#f8fafc !important; +} +select:disabled { + color:#94a3b8 !important; + background:#070b12 !important; +} diff --git a/admin/assets/app.js b/admin/assets/app.js index 7870a49..f1c05ee 100644 --- a/admin/assets/app.js +++ b/admin/assets/app.js @@ -9,6 +9,7 @@ let tlsForwardersState = []; let managedTlsForwardersState = []; let editingXrayClientId = null; let wzInbounds = []; +let wzLoadedFullConfig = null; let dashboardCache = { sshUsers: [], xrayInbounds: [], me: null }; let currentTab = "dashboard"; let inboundsRefreshInFlight = false; @@ -352,6 +353,18 @@ function withServerParam(path, serverID) { function selectedSSHServer() { return sshServerSelect?.value || selectedSSHServerID || "local"; } function selectedXrayServer() { return xrayServerSelect?.value || selectedXrayServerID || "local"; } function serverByID(id) { return serversCache.find(s => String(s.id) === String(id)); } +function selectedXrayServerLabel() { + const id = selectedXrayServer(); + const srv = serverByID(id); + if (srv) return srv.name || srv.base_url || id; + return id === "local" ? "Master node" : id; +} +function reloadXrayConfigForSelectedServer() { + const wizPane = document.getElementById("xrayWizardPane"); + const jsonPane = document.getElementById("xrayCfgPaneJson"); + if (jsonPane && !jsonPane.classList.contains("hidden")) return loadXrayCfg(); + if (wizPane && !wizPane.classList.contains("hidden")) return loadWizardFromConfig(); +} // ─── Formatters ────────────────────────────────────────────────────────────── const fmtPct = n => (n == null || isNaN(n)) ? "--%" : n.toFixed(1)+"%"; @@ -1221,31 +1234,35 @@ async function removeClient(tag, uuid) { } async function loadXrayCfg() { + if (!xCfgEditor) return; + const target = selectedXrayServerLabel(); + if (xCfgStatus) xCfgStatus.textContent = `Loading config from ${target}…`; try { const res = await api(withServerParam("/api/xray/config", selectedXrayServer())); if (!res.ok) throw new Error(await res.text()); const text = await res.text(); try { xCfgEditor.value = JSON.stringify(JSON.parse(text), null, 2); } catch { xCfgEditor.value = text; } - xCfgStatus.textContent = t("Config loaded."); + if (xCfgStatus) xCfgStatus.textContent = `Config loaded from ${target}.`; } catch (e) { if (e.message==="auth") doAuthError(); - else xCfgStatus.textContent = t("Error: {error}", {error: e.message}); + else if (xCfgStatus) xCfgStatus.textContent = t("Error: {error}", {error: e.message}); } } async function saveXrayCfg() { - const text = xCfgEditor.value.trim(); - try { JSON.parse(text); } catch(e) { xCfgStatus.textContent = t("Invalid JSON: {error}", {error: e.message}); return; } - xCfgStatus.textContent = t("Saving…"); + const text = (xCfgEditor?.value || "").trim(); + const target = selectedXrayServerLabel(); + try { JSON.parse(text); } catch(e) { if (xCfgStatus) xCfgStatus.textContent = t("Invalid JSON: {error}", {error: e.message}); return; } + if (xCfgStatus) xCfgStatus.textContent = `Saving config to ${target}…`; try { const res = await api(withServerParam("/api/xray/config", selectedXrayServer()), { method:"POST", body: text }); if (!res.ok) throw new Error(await res.text()); - xCfgStatus.textContent = t("Saved. Restarting Xray…"); + if (xCfgStatus) xCfgStatus.textContent = `Saved on ${target}. Restarting Xray…`; await xrayCtrl("restart"); } catch (e) { if (e.message==="auth") doAuthError(); - else xCfgStatus.textContent = t("Error: {error}", {error: e.message}); + else if (xCfgStatus) xCfgStatus.textContent = t("Error: {error}", {error: e.message}); } } @@ -1382,8 +1399,11 @@ xrayServerSelect?.addEventListener("change", () => { localStorage.setItem("XRAY_SERVER_ID", selectedXrayServerID); lastInboundsStructure = ""; closeEditXrayClient?.(); + if (xStatus) xStatus.textContent = `Switched Xray target to ${selectedXrayServerLabel()}.`; loadXrayStatus(); loadInbounds({ force: true }); + reloadXrayConfigForSelectedServer(); + loadXrayLogs(); }); document.getElementById("reloadServersBtn")?.addEventListener("click", loadServers); document.getElementById("reloadServersBtn2")?.addEventListener("click", loadServers); @@ -1423,17 +1443,17 @@ async function loadServersStatus(options = {}) { if (!serversStatusGrid) return; try { if (!Array.isArray(serversCache) || serversCache.length === 0) await loadServers(); - const nodes = (serversCache || []).filter(s => s && s.is_active !== false); + const nodes = (serversCache || []).filter(Boolean); if (serversStatusCountChip) serversStatusCountChip.textContent = String(nodes.length); if (!silent) { - serversStatusPageStatus && (serversStatusPageStatus.textContent = "Loading server usage..."); - serversStatusGrid.innerHTML = `
Loading nodes...
`; + serversStatusPageStatus && (serversStatusPageStatus.textContent = "Loading servers..."); + serversStatusGrid.innerHTML = `
Loading servers...
`; } const rows = await Promise.all(nodes.map(loadSingleServerStatus)); renderServersStatusCards(rows); if (serversStatusPageStatus) { const online = rows.filter(r => r.ok).length; - serversStatusPageStatus.textContent = `${online}/${rows.length} nodes online - Updated ${new Date().toLocaleTimeString()}`; + serversStatusPageStatus.textContent = `${online}/${rows.length} servers online - Updated ${new Date().toLocaleTimeString()}`; } } catch (e) { if (e.message === "auth") doAuthError(); @@ -1453,6 +1473,7 @@ async function fetchJSONForServer(path, serverID) { async function loadSingleServerStatus(server) { const id = String(server.id || "local"); const out = { server, ok: true, error: "", stats: null, users: [], inbounds: [], xray: null }; + if (server.is_active === false) { out.ok = false; out.error = "disabled"; return out; } try { out.stats = await fetchJSONForServer("/api/stats", id); } catch (e) { @@ -1556,8 +1577,8 @@ function renderServerSelectors() { const hasMultiXray = xrayServers.length > 1; sshServerPickerCard?.classList.toggle("hidden", !hasMultiSSH); xrayServerPickerCard?.classList.toggle("hidden", !hasMultiXray); - if (sshServerHint) sshServerHint.textContent = hasMultiSSH ? "Choose where SSH users are created and listed." : "Only the master node is available for SSH."; - if (xrayServerHint) xrayServerHint.textContent = hasMultiXray ? "Choose where Xray clients are created and listed." : "Only the master node is available for Xray."; + if (sshServerHint) { sshServerHint.textContent = ""; sshServerHint.classList.add("hidden"); } + if (xrayServerHint) { xrayServerHint.textContent = ""; xrayServerHint.classList.add("hidden"); } if (dashServers) dashServers.textContent = String(active.length || 1); if (dashServerStatus) dashServerStatus.textContent = active.length > 1 ? `${active.length} nodes configured` : "master only"; } @@ -2591,16 +2612,25 @@ function setXrayCfgMode(mode) { } } +document.getElementById("wzLogLevel")?.addEventListener("change", updateFullConfigFromWizard); + function loadWizardFromConfig() { - api("/api/xray/config").then(async res => { - if (!res.ok) return; - try { - const cfg = JSON.parse(await res.text()); - document.getElementById("wzLogLevel").value = cfg.log?.loglevel || "warning"; - wzInbounds = (cfg.inbounds || []).filter(ib => ib.tag !== "api"); - renderWzInbounds(); - } catch {} - }).catch(() => {}); + const target = selectedXrayServerLabel(); + const st = document.getElementById("wzStatus"); + if (st) st.textContent = `Loading config from ${target}...`; + api(withServerParam("/api/xray/config", selectedXrayServer())).then(async res => { + if (!res.ok) throw new Error(await res.text()); + const raw = await res.text(); + const cfg = JSON.parse(raw); + wzLoadedFullConfig = cfg; + document.getElementById("wzLogLevel").value = cfg.log?.loglevel || "warning"; + wzInbounds = (cfg.inbounds || []).filter(ib => ib.tag !== "api"); + renderWzInbounds(); + if (st) st.textContent = `Config loaded from ${target}.`; + }).catch(e => { + if (e.message === "auth") doAuthError(); + else if (st) st.textContent = "Error: " + e.message; + }); } function renderWzInbounds() { @@ -2633,7 +2663,7 @@ function renderWzInbounds() { const delBtn = document.createElement("button"); delBtn.className = "btn btn-danger btn-sm"; delBtn.textContent = "Remove"; - delBtn.onclick = () => { wzInbounds.splice(i,1); renderWzInbounds(); }; + delBtn.onclick = () => { wzInbounds.splice(i,1); renderWzInbounds(); updateFullConfigFromWizard(); }; row.appendChild(delBtn); list.appendChild(row); }); @@ -2784,44 +2814,66 @@ function wzSaveInbound() { wzInbounds.push(ib); renderWzInbounds(); document.getElementById("wzAddInboundForm").classList.add("hidden"); + updateFullConfigFromWizard(); document.getElementById("wzPort").value = ""; document.getElementById("wzTag").value = ""; document.getElementById("wzListenIP").value = ""; } -async function applyWizardConfig() { - const st = document.getElementById("wzStatus"); - st.textContent = "Saving…"; - const apiInbound = { + +function ensureXrayApiInbound() { + return { tag: "api", listen: "127.0.0.1", port: 10085, protocol: "dokodemo-door", settings: { address: "127.0.0.1" } }; - const userInbounds = (wzInbounds || []).filter(ib => ib.tag !== "api"); - const cfg = { - log: { loglevel: document.getElementById("wzLogLevel").value }, - api: { tag: "api", services: ["HandlerService", "LoggerService", "StatsService"] }, - stats: {}, - policy: { levels: { "0": { statsUserUplink: true, statsUserDownlink: true } }, system: { statsInboundUplink: true, statsInboundDownlink: true } }, - inbounds: [apiInbound, ...userInbounds], - outbounds: [ - { tag:"direct", protocol:"freedom", settings:{} }, - { tag:"blocked", protocol:"blackhole", settings:{} }, - { tag:"api", protocol:"freedom", settings:{} } - ], - routing: { rules: [{ type: "field", inboundTag: ["api"], outboundTag: "api" }] } - }; +} + +function updateFullConfigFromWizard() { + const cfg = wzLoadedFullConfig && typeof wzLoadedFullConfig === "object" ? JSON.parse(JSON.stringify(wzLoadedFullConfig)) : {}; + cfg.log = cfg.log || {}; + cfg.log.loglevel = document.getElementById("wzLogLevel")?.value || cfg.log.loglevel || "warning"; + const existingInbounds = Array.isArray(cfg.inbounds) ? cfg.inbounds : []; + const apiInbound = existingInbounds.find(ib => ib && ib.tag === "api") || ensureXrayApiInbound(); + cfg.inbounds = [apiInbound, ...(wzInbounds || []).filter(ib => ib && ib.tag !== "api")]; + if (!cfg.api) cfg.api = { tag: "api", services: ["HandlerService", "LoggerService", "StatsService"] }; + if (!cfg.stats) cfg.stats = {}; + if (!cfg.policy) cfg.policy = { levels: { "0": { statsUserUplink: true, statsUserDownlink: true } }, system: { statsInboundUplink: true, statsInboundDownlink: true } }; + if (!cfg.outbounds) cfg.outbounds = [ + { tag:"direct", protocol:"freedom", settings:{} }, + { tag:"blocked", protocol:"blackhole", settings:{} }, + { tag:"api", protocol:"freedom", settings:{} } + ]; + if (!cfg.routing) cfg.routing = { rules: [{ type: "field", inboundTag: ["api"], outboundTag: "api" }] }; + wzLoadedFullConfig = cfg; + return cfg; +} + +async function applyWizardConfig() { + const st = document.getElementById("wzStatus"); + const target = selectedXrayServerLabel(); + let cfg; try { - const res = await api("/api/xray/config", { method:"POST", body: JSON.stringify(cfg, null, 2) }); + cfg = updateFullConfigFromWizard(); + if (!cfg || typeof cfg !== "object") throw new Error("config is not loaded"); + } catch(e) { + if (st) st.textContent = `Invalid visual config: ${e.message}`; + return; + } + if (st) st.textContent = `Saving config to ${target}...`; + try { + const res = await api(withServerParam("/api/xray/config", selectedXrayServer()), { method:"POST", body: JSON.stringify(cfg, null, 2) }); if (!res.ok) throw new Error(await res.text()); - st.textContent = "Saved. Restarting Xray…"; + wzLoadedFullConfig = cfg; + if (st) st.textContent = `Saved on ${target}. Restarting Xray...`; await xrayCtrl("restart"); - st.textContent = "Config saved and Xray restarted."; + if (st) st.textContent = `Config saved on ${target} and Xray restarted.`; + setTimeout(() => { loadXrayStatus(); loadInbounds({ force: true }); }, 700); } catch (e) { if (e.message==="auth") doAuthError(); - else st.textContent = "Error: " + e.message; + else if (st) st.textContent = "Error: " + e.message; } } diff --git a/admin/index.html b/admin/index.html index 21a2f52..ebf2bb5 100644 --- a/admin/index.html +++ b/admin/index.html @@ -16,7 +16,7 @@ setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500); })(); - +
@@ -310,10 +310,10 @@ master/slave
-
+
-
Servers with Xray enabled are available here.
+ @@ -789,8 +789,7 @@
- Small usage graphs for every active master/slave node. - Auto-refreshes while this page is open. + Ready.
@@ -1116,6 +1115,6 @@ - +