From 1ad8b868aba6ef9f0bba966b5c16c01333481ee9 Mon Sep 17 00:00:00 2001 From: penguinehis Date: Mon, 11 May 2026 21:52:07 -0300 Subject: [PATCH] FIx mult panel server --- admin/assets/app.css | 189 +++++++++++++++++++++++++++++++++++++++++++ admin/assets/app.js | 137 +++++++++++++++++++++++++++++++ admin/index.html | 30 +++++-- main.go | 3 + 4 files changed, 353 insertions(+), 6 deletions(-) diff --git a/admin/assets/app.css b/admin/assets/app.css index c3694d3..f248406 100644 --- a/admin/assets/app.css +++ b/admin/assets/app.css @@ -402,3 +402,192 @@ body.drawer-open .drawer-backdrop,body.sidebar-open .drawer-backdrop{display:blo table{font-size:.76rem;} th,td{padding:9px 10px;} } + +/* --- UI polish fixes for servers page / sidebar / language selector --- */ +@media(min-width:901px){ + .panel-layout{align-items:start;} + .sidebar{align-self:start; position:sticky; top:18px;} +} + +/* Keep the sidebar visible while long pages scroll */ +.sidebar{ + overflow:hidden; +} +.side-nav{ + overscroll-behavior:contain; +} + +/* Better top alignment for paired cards */ +.grid2, +.servers-grid{ + align-items:start; +} +.grid2 > .card, +.grid2 > div, +.servers-grid > .card, +.servers-grid > div{ + align-self:start; + margin-top:0 !important; +} + +/* Server form checkbox rows should align visually with the input fields */ +.server-form-grid{ + align-items:start; +} +.server-form-grid > .toggle-field{ + min-height:44px; + display:flex; + align-items:center; + gap:8px; + padding:0 12px; + border:1px solid var(--line); + border-radius:14px; + background:linear-gradient(180deg,var(--input-bg),#06090f); + color:var(--text-2); + font-size:.8rem; + font-weight:800; + cursor:pointer; +} +.server-form-grid > .toggle-field input{ + flex:0 0 auto; +} + +/* Language selector dark theme fix */ +.language-select{ + color:var(--text); + background:linear-gradient(180deg,rgba(17,23,32,.94),rgba(10,14,21,.98)); + border-color:rgba(148,163,184,.18); +} +.language-select:hover, +.language-select:focus{ + border-color:rgba(34,211,238,.42); + box-shadow:0 0 0 3px rgba(34,211,238,.10), 0 0 22px rgba(34,211,238,.08); +} +.language-select option, +.language-select optgroup{ + background:#0d1118; + color:#f3f7ff; +} + +/* Small visual consistency improvements */ +.topbar-actions{ + align-items:center; +} +.card-hdr{ + align-items:flex-start; +} +.card-hdr > .card-actions{ + align-items:center; +} + + +/* --- sidebar follow-scroll fix --- */ +@media(min-width:901px){ + .panel-layout{ + display:block; + padding:18px; + } + .sidebar{ + position:fixed !important; + top:18px !important; + left:18px !important; + bottom:auto !important; + width:300px !important; + height:calc(100vh - 36px) !important; + max-height:calc(100vh - 36px) !important; + z-index:30; + } + @supports (height:100dvh){ + .sidebar{ + height:calc(100dvh - 36px) !important; + max-height:calc(100dvh - 36px) !important; + } + } + .workspace{ + margin-left:336px !important; + min-height:calc(100vh - 36px); + } + @supports (min-height:100dvh){ + .workspace{min-height:calc(100dvh - 36px);} + } +} + + +/* --- Servers status page --- */ +.servers-status-toolbar{margin-bottom:16px;} +.servers-status-grid{ + display:grid; + grid-template-columns:repeat(auto-fit,minmax(330px,1fr)); + gap:16px; + align-items:start; +} +.server-status-card{ + position:relative; + overflow:hidden; + border:1px solid rgba(148,163,184,.12); + border-radius:24px; + padding:16px; + background: + radial-gradient(circle at 90% 0%,rgba(255,255,255,.08),transparent 34%), + linear-gradient(180deg,rgba(16,22,32,.95),rgba(8,12,18,.98)); + box-shadow:0 20px 58px rgba(0,0,0,.26),inset 0 1px 0 rgba(255,255,255,.025); +} +.server-status-card::after{ + content:""; + position:absolute; + right:-38px; + bottom:-58px; + width:150px; + height:150px; + border-radius:999px; + background:rgba(34,211,238,.12); + pointer-events:none; +} +.server-status-offline{opacity:.72;} +.server-status-offline::after{background:rgba(255,91,105,.11);} +.server-status-head{ + position:relative; + z-index:1; + display:flex; + align-items:flex-start; + justify-content:space-between; + gap:12px; + margin-bottom:12px; +} +.server-status-title{font-size:1rem;font-weight:950;color:var(--text);line-height:1.1;} +.server-status-url{margin-top:5px;color:var(--muted);font-size:.72rem;font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;word-break:break-all;} +.server-status-badges{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap;} +.server-status-error{position:relative;z-index:1;margin-bottom:10px;color:#ffc6cc;font-size:.76rem;} +.server-mini-grid{ + position:relative; + z-index:1; + display:grid; + grid-template-columns:repeat(2,minmax(0,1fr)); + gap:10px; +} +.server-mini-metric{ + min-width:0; + border:1px solid rgba(148,163,184,.11); + border-radius:18px; + padding:12px; + background:rgba(255,255,255,.035); +} +.server-mini-label{color:var(--muted);font-size:.66rem;text-transform:uppercase;letter-spacing:.14em;font-weight:900;} +.server-mini-value{margin-top:6px;font-size:1.28rem;line-height:1.05;font-weight:950;color:var(--text);letter-spacing:-.04em;} +.server-mini-note{margin-top:5px;min-height:15px;color:var(--muted);font-size:.7rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} +.server-mini-bar{height:7px;margin-top:9px;border-radius:999px;background:rgba(148,163,184,.12);overflow:hidden;} +.server-mini-bar span{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,var(--accent),var(--accent-3));box-shadow:0 0 18px rgba(34,211,238,.24);transition:width .25s ease;} +.server-status-footer{ + position:relative; + z-index:1; + display:grid; + gap:5px; + margin-top:12px; + color:var(--muted); + font-size:.72rem; + line-height:1.35; +} +@media(max-width:640px){ + .servers-status-grid{grid-template-columns:1fr;} + .server-mini-grid{grid-template-columns:1fr;} +} diff --git a/admin/assets/app.js b/admin/assets/app.js index eae2a53..7870a49 100644 --- a/admin/assets/app.js +++ b/admin/assets/app.js @@ -308,6 +308,9 @@ const serverConfigSubpage = document.getElementById("serverConfigSubpage"); const cfgServerName = document.getElementById("cfgServerName"); const managedConfigEditor = document.getElementById("managedConfigEditor"); const managedConfigStatus = document.getElementById("managedConfigStatus"); +const serversStatusGrid = document.getElementById("serversStatusGrid"); +const serversStatusPageStatus = document.getElementById("serversStatusPageStatus"); +const serversStatusCountChip = document.getElementById("serversStatusCountChip"); // Stats const cpuVal = document.getElementById("cpuVal"); @@ -491,6 +494,7 @@ const tabTitles = { xray: ["Accounts", "Xray Users"], resellers: ["Administration", "Resellers"], servers: ["Administration", "Servers"], + "servers-status": ["Administration", "Servers Status"], stats: ["Server", "Monitoring"], vnstat: ["Traffic", "VnStat"], logs: ["System", "Logs"], @@ -521,6 +525,7 @@ function selectTab(tab) { if (currentRole === "superadmin") loadWizardFromConfig(); } if (tab === "stats" && currentRole === "superadmin") loadStats(); + if (tab === "servers-status" && currentRole === "superadmin") loadServersStatus(); if (tab === "resellers" && currentRole === "superadmin") loadResellers(); if (tab === "servers" && currentRole === "superadmin") loadServers(); } @@ -612,6 +617,7 @@ function initAfterLogin() { statsTimer = setInterval(() => { loadDashboardStats(); if (currentTab === "stats") loadStats(); + if (currentTab === "servers-status") loadServersStatus({ silent: true }); }, 2000); } else { loadMe(); @@ -1382,7 +1388,9 @@ xrayServerSelect?.addEventListener("change", () => { document.getElementById("reloadServersBtn")?.addEventListener("click", loadServers); document.getElementById("reloadServersBtn2")?.addEventListener("click", loadServers); document.getElementById("refreshServersBtn")?.addEventListener("click", loadServers); +document.getElementById("refreshServersStatusBtn")?.addEventListener("click", () => loadServersStatus()); document.querySelector("[data-tab='servers']")?.addEventListener("click", loadServers); +document.querySelector("[data-tab='servers-status']")?.addEventListener("click", () => loadServersStatus()); document.getElementById("clearServerFormBtn")?.addEventListener("click", clearServerForm); document.getElementById("testServerBtn")?.addEventListener("click", testServerForm); document.getElementById("backToServersBtn")?.addEventListener("click", () => showServerListView()); @@ -1409,6 +1417,135 @@ async function loadServers() { renderServersTable(); } + +async function loadServersStatus(options = {}) { + const silent = !!options.silent; + if (!serversStatusGrid) return; + try { + if (!Array.isArray(serversCache) || serversCache.length === 0) await loadServers(); + const nodes = (serversCache || []).filter(s => s && s.is_active !== false); + if (serversStatusCountChip) serversStatusCountChip.textContent = String(nodes.length); + if (!silent) { + serversStatusPageStatus && (serversStatusPageStatus.textContent = "Loading server usage..."); + serversStatusGrid.innerHTML = `
Loading nodes...
`; + } + 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()}`; + } + } catch (e) { + if (e.message === "auth") doAuthError(); + else { + serversStatusPageStatus && (serversStatusPageStatus.textContent = "Error loading server status: " + e.message); + if (!silent) serversStatusGrid.innerHTML = `
Error loading server status.
`; + } + } +} + +async function fetchJSONForServer(path, serverID) { + const res = await api(withServerParam(path, serverID)); + if (!res.ok) throw new Error((await res.text()).trim() || `HTTP ${res.status}`); + return await res.json(); +} + +async function loadSingleServerStatus(server) { + const id = String(server.id || "local"); + const out = { server, ok: true, error: "", stats: null, users: [], inbounds: [], xray: null }; + try { + out.stats = await fetchJSONForServer("/api/stats", id); + } catch (e) { + out.ok = false; + out.error = e.message || "stats failed"; + } + if (server.enable_ssh || server.is_local) { + try { out.users = await fetchJSONForServer("/api/users", id) || []; } + catch (e) { out.usersError = e.message || "users failed"; } + } + if (server.enable_xray || server.is_local) { + try { out.xray = await fetchJSONForServer("/api/xray/status", id); } + catch (e) { out.xrayError = e.message || "xray status failed"; } + try { out.inbounds = await fetchJSONForServer("/api/xray/inbounds", id) || []; } + catch (e) { out.inboundsError = e.message || "xray clients failed"; } + } + return out; +} + +function renderServersStatusCards(rows = []) { + if (!serversStatusGrid) return; + if (!rows.length) { + serversStatusGrid.innerHTML = `
No active servers configured.
`; + return; + } + serversStatusGrid.innerHTML = rows.map(serverStatusCardHTML).join(""); +} + +function serverStatusCardHTML(row) { + const s = row.server || {}; + const stats = row.stats || {}; + const ifaces = Array.isArray(stats.interfaces) ? stats.interfaces : []; + let rx = 0, tx = 0, rxTotal = 0, txTotal = 0; + ifaces.forEach(it => { + rx += Number(it.rx_mbps || 0); + tx += Number(it.tx_mbps || 0); + rxTotal += Number(it.rx_bytes || 0); + txTotal += Number(it.tx_bytes || 0); + }); + const cpu = Number(stats.cpu_percent || 0); + const mem = stats.mem_percent == null ? 0 : Number(stats.mem_percent || 0); + const users = Array.isArray(row.users) ? row.users : []; + const now = Date.now(); + const sshActive = users.filter(u => !u.expires_at || new Date(u.expires_at).getTime() > now).length; + const sshExpired = Math.max(0, users.length - sshActive); + const sshConns = users.reduce((sum, u) => sum + Number(u.active_conns || 0), 0); + const clients = []; + (Array.isArray(row.inbounds) ? row.inbounds : []).forEach(ib => (ib.clients || []).forEach(c => clients.push(c))); + const xrayOnline = clients.filter(c => !!c.online).length; + const xrayActive = clients.filter(c => !c.expired && (!c.expires_at || new Date(c.expires_at).getTime() > now)).length; + const xrayExpired = Math.max(0, clients.length - xrayActive); + const netNow = rx + tx; + const running = row.xray ? !!row.xray.running : false; + const nodeStatus = row.ok ? `online` : `offline`; + const options = `${s.enable_ssh ? "SSH" : ""}${s.enable_ssh && s.enable_xray ? " / " : ""}${s.enable_xray ? "Xray" : ""}` || "disabled"; + const err = row.ok ? "" : `
${escapeHTML(row.error || "connection failed")}
`; + return ` +
+
+
+
${escapeHTML(s.name || "Server")}
+
${escapeHTML(s.base_url || "local")}
+
+
+ ${nodeStatus} + ${escapeHTML(options)} +
+
+ ${err} +
+ ${miniMetricHTML("CPU", fmtPct(cpu), cpu, cpu >= 85 ? "High load" : cpu >= 60 ? "Moderate load" : "Normal load")} + ${miniMetricHTML("RAM", fmtPct(mem), mem, stats.mem_used_bytes && stats.mem_total_bytes ? `${fmtBytes(stats.mem_used_bytes)} / ${fmtBytes(stats.mem_total_bytes)}` : "Memory used")} + ${miniMetricHTML("Network", `${fmtMbps(netNow)} Mb/s`, Math.min(100, netNow / 20), `RX ${fmtMbps(rx)} - TX ${fmtMbps(tx)}`)} + ${miniMetricHTML("Accounts", String(users.length + clients.length), Math.min(100, (users.length + clients.length) * 3), `SSH ${users.length} - Xray ${clients.length}`)} +
+ +
`; +} + +function miniMetricHTML(label, value, pct, note) { + const width = Math.min(100, Math.max(0, Number(pct) || 0)); + return `
+
${escapeHTML(label)}
+
${escapeHTML(value)}
+
${escapeHTML(note || "")}
+
+
`; +} + function renderServerSelectors() { const active = serversCache.filter(s => s.is_active !== false); const sshServers = active.filter(s => s.enable_ssh || s.is_local); diff --git a/admin/index.html b/admin/index.html index 55495e1..21a2f52 100644 --- a/admin/index.html +++ b/admin/index.html @@ -16,7 +16,7 @@ setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500); })(); - +
@@ -53,6 +53,7 @@ + @@ -580,7 +581,7 @@
-
+
Managed servers 0
@@ -599,14 +600,14 @@
Add / edit server
-
+
- - - + + +
@@ -778,6 +779,23 @@
+ +
+
+
+
Servers Status 0
+
+ +
+
+
+ Small usage graphs for every active master/slave node. + Auto-refreshes while this page is open. +
+
+
+
+
diff --git a/main.go b/main.go index 97e02a9..120c6d1 100644 --- a/main.go +++ b/main.go @@ -1698,6 +1698,9 @@ func handleStats(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusMethodNotAllowed) return } + if proxyManagedServerFromRequest(w, r, statsStore, "/api/stats", nil, "") { + return + } stats := getCurrentStats() w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(stats)