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 = `