diff --git a/admin/assets/app.css b/admin/assets/app.css index 18ba131..5d007e8 100644 --- a/admin/assets/app.css +++ b/admin/assets/app.css @@ -298,3 +298,32 @@ pre.log-box{background:#121214;border-color:var(--border);border-radius:15px;} @media(max-width:620px){.mini-summary{grid-template-columns:1fr}.mini-meter{max-width:none}} .table-meter{height:6px;min-width:130px;max-width:220px;background:rgba(255,255,255,.07);border:1px solid var(--border);border-radius:999px;overflow:hidden;margin-top:6px;} .table-meter span{display:block;height:100%;background:linear-gradient(90deg,#36d37a,#ffbe4c,#ff7070);border-radius:inherit;} + +/* Final responsive hardening */ +.card-hdr{align-items:flex-start;max-width:100%;} +.card-hdr .card-title{flex:1 1 220px;min-width:0;} +.card-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;max-width:100%;min-width:0;} +.card-actions .btn{white-space:nowrap;} +.dashboard-meter{max-width:230px;} +.save-bar{margin-top:18px;padding:14px 16px;border:1px solid var(--border);border-radius:18px;background:linear-gradient(180deg,var(--card2),var(--card));box-shadow:var(--shadow);display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;} +.save-bar-actions{justify-content:flex-start;} +.topbar-actions{min-width:0;} +.welcome-actions .btn{white-space:nowrap;} +@media(max-width:820px){ + .card-hdr .card-title{flex-basis:100%;} + .card-actions{width:100%;justify-content:flex-start;} + .card-actions .btn{flex:1 1 auto;justify-content:center;} + .save-bar{align-items:stretch;} + .save-bar-actions{width:100%;} + .save-bar-actions .btn{flex:1 1 130px;} + .welcome-actions{width:100%;justify-content:flex-start;} + .welcome-actions .btn{flex:1 1 150px;justify-content:center;} +} +@media(max-width:420px){ + .topbar-left{min-width:0;} + .workspace-main{padding-left:10px;padding-right:10px;} + .dash-card{min-height:132px;padding:18px;} + .dash-card small{font-size:.78rem;} + .dash-icon{width:54px;height:54px;font-size:1.35rem;} + .btn{padding:9px 11px;} +} diff --git a/admin/assets/app.js b/admin/assets/app.js index 61b9836..a7b70c8 100644 --- a/admin/assets/app.js +++ b/admin/assets/app.js @@ -37,6 +37,15 @@ const dashServers = document.getElementById("dashServers"); const dashServerStatus = document.getElementById("dashServerStatus"); const dashXrayClients = document.getElementById("dashXrayClients"); const dashXrayStatus = document.getElementById("dashXrayStatus"); +const dashCpuVal = document.getElementById("dashCpuVal"); +const dashCpuText = document.getElementById("dashCpuText"); +const dashCpuBar = document.getElementById("dashCpuBar"); +const dashRamVal = document.getElementById("dashRamVal"); +const dashRamText = document.getElementById("dashRamText"); +const dashRamBar = document.getElementById("dashRamBar"); +const dashNetVal = document.getElementById("dashNetVal"); +const dashNetText = document.getElementById("dashNetText"); +const dashNetTotal = document.getElementById("dashNetTotal"); const dashQuotaChip = document.getElementById("dashQuotaChip"); const dashQuotaBar = document.getElementById("dashQuotaBar"); const dashQuotaText = document.getElementById("dashQuotaText"); @@ -85,6 +94,7 @@ const xRunning = document.getElementById("xRunning"); const xPID = document.getElementById("xPID"); const xUptime = document.getElementById("xUptime"); const xStatus = document.getElementById("xStatus"); +const xOnlineUsers = document.getElementById("xOnlineUsers"); const xCfgEditor = document.getElementById("xCfgEditor"); const xCfgStatus = document.getElementById("xCfgStatus"); const xLogsBox = document.getElementById("xLogsBox"); @@ -468,6 +478,7 @@ function refreshDashboard() { loadUsersSilent(); loadInbounds(); loadXrayStatus(); + if (currentRole === "superadmin") loadStats(); if (currentRole === "reseller") loadMe(); } @@ -634,7 +645,7 @@ document.getElementById("xStartBtn").addEventListener("click", () => xrayCtrl("s document.getElementById("xStopBtn").addEventListener("click", () => xrayCtrl("stop")); document.getElementById("xRestartBtn").addEventListener("click", () => xrayCtrl("restart")); document.getElementById("xRepairStatsBtn")?.addEventListener("click", repairXrayStats); -document.getElementById("xRefreshBtn").addEventListener("click", loadXrayStatus); +document.getElementById("xRefreshBtn").addEventListener("click", () => { loadXrayStatus(); loadInbounds(); }); document.getElementById("xLoadInboundsBtn").addEventListener("click", loadInbounds); document.getElementById("xLoadCfgBtn").addEventListener("click", loadXrayCfg); document.getElementById("xSaveCfgBtn").addEventListener("click", saveXrayCfg); @@ -727,6 +738,24 @@ async function loadInbounds() { } } +async function copyText(text) { + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + return true; + } + } catch {} + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.left = "-9999px"; + document.body.appendChild(ta); + ta.focus(); + ta.select(); + try { return document.execCommand("copy"); } + finally { document.body.removeChild(ta); } +} + function renderInbounds(inbounds) { updateDashboardXray(inbounds); if (!inbounds.length) { @@ -815,7 +844,7 @@ function renderInbounds(inbounds) { const copyBtn = document.createElement("button"); copyBtn.className = "btn btn-ghost btn-sm"; copyBtn.textContent = "Copy"; - copyBtn.onclick = () => navigator.clipboard.writeText(c.id); + copyBtn.onclick = async () => { await copyText(c.id); xStatus.textContent = "Copied client ID."; }; const editBtn = document.createElement("button"); editBtn.className = "btn btn-warn btn-sm"; editBtn.style.marginLeft = "4px"; @@ -1043,10 +1072,37 @@ async function deleteReseller(username) { // ─── Stats ──────────────────────────────────────────────────────────────────── document.querySelector("[data-tab='stats']")?.addEventListener("click", loadStats); +function updateDashboardStats(s) { + if (!s) return; + const cpu = Number(s.cpu_percent ?? 0); + const mem = s.mem_percent == null ? null : Number(s.mem_percent); + if (dashCpuVal) dashCpuVal.textContent = fmtPct(cpu); + if (dashCpuBar) dashCpuBar.style.width = Math.min(100, Math.max(0, cpu)) + "%"; + if (dashCpuText) dashCpuText.textContent = cpu >= 85 ? "Carga alta" : cpu >= 60 ? "Carga moderada" : "Carga normal"; + if (dashRamVal) dashRamVal.textContent = mem == null ? "--%" : fmtPct(mem); + if (dashRamBar) dashRamBar.style.width = mem == null ? "0%" : Math.min(100, Math.max(0, mem)) + "%"; + if (dashRamText) { + const used = s.mem_used_bytes, total = s.mem_total_bytes; + dashRamText.textContent = used != null && total != null ? `${fmtBytes(used)} / ${fmtBytes(total)}` : "Memória usada"; + } + const ifaces = Array.isArray(s.interfaces) ? s.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); + }); + if (dashNetVal) dashNetVal.textContent = `${fmtMbps(rx + tx)} Mb/s`; + if (dashNetText) dashNetText.textContent = `RX ${fmtMbps(rx)} · TX ${fmtMbps(tx)} Mb/s`; + if (dashNetTotal) dashNetTotal.textContent = `Total ${fmtBytes(rxTotal + txTotal)}`; +} + async function loadStats() { try { const res = await api("/api/stats"); const s = await res.json(); + updateDashboardStats(s); const cpu = s?.cpu_percent ?? 0; cpuVal.textContent = fmtPct(cpu); cpuBar.style.width = Math.min(100, Math.max(0, cpu)) + "%"; diff --git a/admin/index.html b/admin/index.html index efb9596..11ab2cb 100644 --- a/admin/index.html +++ b/admin/index.html @@ -64,8 +64,6 @@
+ + +