Mult node launch
This commit is contained in:
@@ -591,3 +591,39 @@ body.drawer-open .drawer-backdrop,body.sidebar-open .drawer-backdrop{display:blo
|
|||||||
.servers-status-grid{grid-template-columns:1fr;}
|
.servers-status-grid{grid-template-columns:1fr;}
|
||||||
.server-mini-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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ let tlsForwardersState = [];
|
|||||||
let managedTlsForwardersState = [];
|
let managedTlsForwardersState = [];
|
||||||
let editingXrayClientId = null;
|
let editingXrayClientId = null;
|
||||||
let wzInbounds = [];
|
let wzInbounds = [];
|
||||||
|
let wzLoadedFullConfig = null;
|
||||||
let dashboardCache = { sshUsers: [], xrayInbounds: [], me: null };
|
let dashboardCache = { sshUsers: [], xrayInbounds: [], me: null };
|
||||||
let currentTab = "dashboard";
|
let currentTab = "dashboard";
|
||||||
let inboundsRefreshInFlight = false;
|
let inboundsRefreshInFlight = false;
|
||||||
@@ -352,6 +353,18 @@ function withServerParam(path, serverID) {
|
|||||||
function selectedSSHServer() { return sshServerSelect?.value || selectedSSHServerID || "local"; }
|
function selectedSSHServer() { return sshServerSelect?.value || selectedSSHServerID || "local"; }
|
||||||
function selectedXrayServer() { return xrayServerSelect?.value || selectedXrayServerID || "local"; }
|
function selectedXrayServer() { return xrayServerSelect?.value || selectedXrayServerID || "local"; }
|
||||||
function serverByID(id) { return serversCache.find(s => String(s.id) === String(id)); }
|
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 ──────────────────────────────────────────────────────────────
|
// ─── Formatters ──────────────────────────────────────────────────────────────
|
||||||
const fmtPct = n => (n == null || isNaN(n)) ? "--%" : n.toFixed(1)+"%";
|
const fmtPct = n => (n == null || isNaN(n)) ? "--%" : n.toFixed(1)+"%";
|
||||||
@@ -1221,31 +1234,35 @@ async function removeClient(tag, uuid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadXrayCfg() {
|
async function loadXrayCfg() {
|
||||||
|
if (!xCfgEditor) return;
|
||||||
|
const target = selectedXrayServerLabel();
|
||||||
|
if (xCfgStatus) xCfgStatus.textContent = `Loading config from ${target}…`;
|
||||||
try {
|
try {
|
||||||
const res = await api(withServerParam("/api/xray/config", selectedXrayServer()));
|
const res = await api(withServerParam("/api/xray/config", selectedXrayServer()));
|
||||||
if (!res.ok) throw new Error(await res.text());
|
if (!res.ok) throw new Error(await res.text());
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
try { xCfgEditor.value = JSON.stringify(JSON.parse(text), null, 2); }
|
try { xCfgEditor.value = JSON.stringify(JSON.parse(text), null, 2); }
|
||||||
catch { xCfgEditor.value = text; }
|
catch { xCfgEditor.value = text; }
|
||||||
xCfgStatus.textContent = t("Config loaded.");
|
if (xCfgStatus) xCfgStatus.textContent = `Config loaded from ${target}.`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message==="auth") doAuthError();
|
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() {
|
async function saveXrayCfg() {
|
||||||
const text = xCfgEditor.value.trim();
|
const text = (xCfgEditor?.value || "").trim();
|
||||||
try { JSON.parse(text); } catch(e) { xCfgStatus.textContent = t("Invalid JSON: {error}", {error: e.message}); return; }
|
const target = selectedXrayServerLabel();
|
||||||
xCfgStatus.textContent = t("Saving…");
|
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 {
|
try {
|
||||||
const res = await api(withServerParam("/api/xray/config", selectedXrayServer()), { method:"POST", body: text });
|
const res = await api(withServerParam("/api/xray/config", selectedXrayServer()), { method:"POST", body: text });
|
||||||
if (!res.ok) throw new Error(await res.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");
|
await xrayCtrl("restart");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message==="auth") doAuthError();
|
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);
|
localStorage.setItem("XRAY_SERVER_ID", selectedXrayServerID);
|
||||||
lastInboundsStructure = "";
|
lastInboundsStructure = "";
|
||||||
closeEditXrayClient?.();
|
closeEditXrayClient?.();
|
||||||
|
if (xStatus) xStatus.textContent = `Switched Xray target to ${selectedXrayServerLabel()}.`;
|
||||||
loadXrayStatus();
|
loadXrayStatus();
|
||||||
loadInbounds({ force: true });
|
loadInbounds({ force: true });
|
||||||
|
reloadXrayConfigForSelectedServer();
|
||||||
|
loadXrayLogs();
|
||||||
});
|
});
|
||||||
document.getElementById("reloadServersBtn")?.addEventListener("click", loadServers);
|
document.getElementById("reloadServersBtn")?.addEventListener("click", loadServers);
|
||||||
document.getElementById("reloadServersBtn2")?.addEventListener("click", loadServers);
|
document.getElementById("reloadServersBtn2")?.addEventListener("click", loadServers);
|
||||||
@@ -1423,17 +1443,17 @@ async function loadServersStatus(options = {}) {
|
|||||||
if (!serversStatusGrid) return;
|
if (!serversStatusGrid) return;
|
||||||
try {
|
try {
|
||||||
if (!Array.isArray(serversCache) || serversCache.length === 0) await loadServers();
|
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 (serversStatusCountChip) serversStatusCountChip.textContent = String(nodes.length);
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
serversStatusPageStatus && (serversStatusPageStatus.textContent = "Loading server usage...");
|
serversStatusPageStatus && (serversStatusPageStatus.textContent = "Loading servers...");
|
||||||
serversStatusGrid.innerHTML = `<div class="hint">Loading nodes...</div>`;
|
serversStatusGrid.innerHTML = `<div class="hint">Loading servers...</div>`;
|
||||||
}
|
}
|
||||||
const rows = await Promise.all(nodes.map(loadSingleServerStatus));
|
const rows = await Promise.all(nodes.map(loadSingleServerStatus));
|
||||||
renderServersStatusCards(rows);
|
renderServersStatusCards(rows);
|
||||||
if (serversStatusPageStatus) {
|
if (serversStatusPageStatus) {
|
||||||
const online = rows.filter(r => r.ok).length;
|
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) {
|
} catch (e) {
|
||||||
if (e.message === "auth") doAuthError();
|
if (e.message === "auth") doAuthError();
|
||||||
@@ -1453,6 +1473,7 @@ async function fetchJSONForServer(path, serverID) {
|
|||||||
async function loadSingleServerStatus(server) {
|
async function loadSingleServerStatus(server) {
|
||||||
const id = String(server.id || "local");
|
const id = String(server.id || "local");
|
||||||
const out = { server, ok: true, error: "", stats: null, users: [], inbounds: [], xray: null };
|
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 {
|
try {
|
||||||
out.stats = await fetchJSONForServer("/api/stats", id);
|
out.stats = await fetchJSONForServer("/api/stats", id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1556,8 +1577,8 @@ function renderServerSelectors() {
|
|||||||
const hasMultiXray = xrayServers.length > 1;
|
const hasMultiXray = xrayServers.length > 1;
|
||||||
sshServerPickerCard?.classList.toggle("hidden", !hasMultiSSH);
|
sshServerPickerCard?.classList.toggle("hidden", !hasMultiSSH);
|
||||||
xrayServerPickerCard?.classList.toggle("hidden", !hasMultiXray);
|
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 (sshServerHint) { sshServerHint.textContent = ""; sshServerHint.classList.add("hidden"); }
|
||||||
if (xrayServerHint) xrayServerHint.textContent = hasMultiXray ? "Choose where Xray clients are created and listed." : "Only the master node is available for Xray.";
|
if (xrayServerHint) { xrayServerHint.textContent = ""; xrayServerHint.classList.add("hidden"); }
|
||||||
if (dashServers) dashServers.textContent = String(active.length || 1);
|
if (dashServers) dashServers.textContent = String(active.length || 1);
|
||||||
if (dashServerStatus) dashServerStatus.textContent = active.length > 1 ? `${active.length} nodes configured` : "master only";
|
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() {
|
function loadWizardFromConfig() {
|
||||||
api("/api/xray/config").then(async res => {
|
const target = selectedXrayServerLabel();
|
||||||
if (!res.ok) return;
|
const st = document.getElementById("wzStatus");
|
||||||
try {
|
if (st) st.textContent = `Loading config from ${target}...`;
|
||||||
const cfg = JSON.parse(await res.text());
|
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";
|
document.getElementById("wzLogLevel").value = cfg.log?.loglevel || "warning";
|
||||||
wzInbounds = (cfg.inbounds || []).filter(ib => ib.tag !== "api");
|
wzInbounds = (cfg.inbounds || []).filter(ib => ib.tag !== "api");
|
||||||
renderWzInbounds();
|
renderWzInbounds();
|
||||||
} catch {}
|
if (st) st.textContent = `Config loaded from ${target}.`;
|
||||||
}).catch(() => {});
|
}).catch(e => {
|
||||||
|
if (e.message === "auth") doAuthError();
|
||||||
|
else if (st) st.textContent = "Error: " + e.message;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWzInbounds() {
|
function renderWzInbounds() {
|
||||||
@@ -2633,7 +2663,7 @@ function renderWzInbounds() {
|
|||||||
const delBtn = document.createElement("button");
|
const delBtn = document.createElement("button");
|
||||||
delBtn.className = "btn btn-danger btn-sm";
|
delBtn.className = "btn btn-danger btn-sm";
|
||||||
delBtn.textContent = "Remove";
|
delBtn.textContent = "Remove";
|
||||||
delBtn.onclick = () => { wzInbounds.splice(i,1); renderWzInbounds(); };
|
delBtn.onclick = () => { wzInbounds.splice(i,1); renderWzInbounds(); updateFullConfigFromWizard(); };
|
||||||
row.appendChild(delBtn);
|
row.appendChild(delBtn);
|
||||||
list.appendChild(row);
|
list.appendChild(row);
|
||||||
});
|
});
|
||||||
@@ -2784,44 +2814,66 @@ function wzSaveInbound() {
|
|||||||
wzInbounds.push(ib);
|
wzInbounds.push(ib);
|
||||||
renderWzInbounds();
|
renderWzInbounds();
|
||||||
document.getElementById("wzAddInboundForm").classList.add("hidden");
|
document.getElementById("wzAddInboundForm").classList.add("hidden");
|
||||||
|
updateFullConfigFromWizard();
|
||||||
document.getElementById("wzPort").value = "";
|
document.getElementById("wzPort").value = "";
|
||||||
document.getElementById("wzTag").value = "";
|
document.getElementById("wzTag").value = "";
|
||||||
document.getElementById("wzListenIP").value = "";
|
document.getElementById("wzListenIP").value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyWizardConfig() {
|
|
||||||
const st = document.getElementById("wzStatus");
|
function ensureXrayApiInbound() {
|
||||||
st.textContent = "Saving…";
|
return {
|
||||||
const apiInbound = {
|
|
||||||
tag: "api",
|
tag: "api",
|
||||||
listen: "127.0.0.1",
|
listen: "127.0.0.1",
|
||||||
port: 10085,
|
port: 10085,
|
||||||
protocol: "dokodemo-door",
|
protocol: "dokodemo-door",
|
||||||
settings: { address: "127.0.0.1" }
|
settings: { address: "127.0.0.1" }
|
||||||
};
|
};
|
||||||
const userInbounds = (wzInbounds || []).filter(ib => ib.tag !== "api");
|
}
|
||||||
const cfg = {
|
|
||||||
log: { loglevel: document.getElementById("wzLogLevel").value },
|
function updateFullConfigFromWizard() {
|
||||||
api: { tag: "api", services: ["HandlerService", "LoggerService", "StatsService"] },
|
const cfg = wzLoadedFullConfig && typeof wzLoadedFullConfig === "object" ? JSON.parse(JSON.stringify(wzLoadedFullConfig)) : {};
|
||||||
stats: {},
|
cfg.log = cfg.log || {};
|
||||||
policy: { levels: { "0": { statsUserUplink: true, statsUserDownlink: true } }, system: { statsInboundUplink: true, statsInboundDownlink: true } },
|
cfg.log.loglevel = document.getElementById("wzLogLevel")?.value || cfg.log.loglevel || "warning";
|
||||||
inbounds: [apiInbound, ...userInbounds],
|
const existingInbounds = Array.isArray(cfg.inbounds) ? cfg.inbounds : [];
|
||||||
outbounds: [
|
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:"direct", protocol:"freedom", settings:{} },
|
||||||
{ tag:"blocked", protocol:"blackhole", settings:{} },
|
{ tag:"blocked", protocol:"blackhole", settings:{} },
|
||||||
{ tag:"api", protocol:"freedom", settings:{} }
|
{ tag:"api", protocol:"freedom", settings:{} }
|
||||||
],
|
];
|
||||||
routing: { rules: [{ type: "field", inboundTag: ["api"], outboundTag: "api" }] }
|
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 {
|
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());
|
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");
|
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) {
|
} catch (e) {
|
||||||
if (e.message==="auth") doAuthError();
|
if (e.message==="auth") doAuthError();
|
||||||
else st.textContent = "Error: " + e.message;
|
else if (st) st.textContent = "Error: " + e.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500);
|
setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="assets/app.css?v=20260511serverstatus1"/>
|
<link rel="stylesheet" href="assets/app.css?v=20260511xrayfullconfig2"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
@@ -310,10 +310,10 @@
|
|||||||
<span class="chip">master/slave</span>
|
<span class="chip">master/slave</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-grid" style="grid-template-columns:1fr auto;align-items:end;">
|
<div class="form-grid" style="grid-template-columns:1fr auto;align-items:end;">
|
||||||
<div class="field"><label>Create/list Xray users on</label><select id="xrayServerSelect"></select></div>
|
<div class="field"><label>Manage Xray users and config on</label><select id="xrayServerSelect"></select></div>
|
||||||
<button class="btn btn-ghost btn-sm" type="button" id="reloadServersBtn2">Reload servers</button>
|
<button class="btn btn-ghost btn-sm" type="button" id="reloadServersBtn2">Reload servers</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="hint" id="xrayServerHint">Servers with Xray enabled are available here.</div>
|
<div class="hint hidden" id="xrayServerHint"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
@@ -789,8 +789,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="statusbar">
|
<div class="statusbar">
|
||||||
<span id="serversStatusPageStatus">Small usage graphs for every active master/slave node.</span>
|
<span id="serversStatusPageStatus">Ready.</span>
|
||||||
<span class="hint">Auto-refreshes while this page is open.</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="servers-status-grid" id="serversStatusGrid"></div>
|
<div class="servers-status-grid" id="serversStatusGrid"></div>
|
||||||
@@ -1116,6 +1115,6 @@
|
|||||||
</div><!-- /shell -->
|
</div><!-- /shell -->
|
||||||
</div><!-- /app -->
|
</div><!-- /app -->
|
||||||
|
|
||||||
<script defer src="assets/app.js?v=20260511servers2"></script>
|
<script defer src="assets/app.js?v=20260511xrayfullconfig2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user