From 4a04ff79f0bab1f124a7d9329bd265139ad9f049 Mon Sep 17 00:00:00 2001 From: penguinehis Date: Sun, 10 May 2026 18:21:03 -0300 Subject: [PATCH] Fix panel --- admin/assets/app.css | 18 +++-- admin/assets/app.js | 188 ++++++++++++++++++++++++++++++++----------- admin/index.html | 4 +- 3 files changed, 158 insertions(+), 52 deletions(-) diff --git a/admin/assets/app.css b/admin/assets/app.css index 5d007e8..382394e 100644 --- a/admin/assets/app.css +++ b/admin/assets/app.css @@ -164,8 +164,10 @@ body.light-mode{ } .app{padding:0;background:linear-gradient(180deg,var(--bg),#101010);min-height:100vh;} .shell{max-width:none;margin:0;background:transparent;border:0;border-radius:0;box-shadow:none;padding:0;min-height:100vh;} -.panel-layout{display:grid;grid-template-columns:280px minmax(0,1fr);min-height:100vh;background:var(--bg);} -.sidebar{position:sticky;top:0;height:100vh;background:var(--panel);border-right:1px solid var(--border);display:flex;flex-direction:column;z-index:20;box-shadow:18px 0 45px rgba(0,0,0,.18);} +.panel-layout{display:block;min-height:100vh;background:var(--bg);} +@supports (min-height:100dvh){.panel-layout{min-height:100dvh;}} +.sidebar{position:fixed;left:0;top:0;bottom:0;width:280px;height:100vh;background:var(--panel);border-right:1px solid var(--border);display:flex;flex-direction:column;z-index:20;box-shadow:18px 0 45px rgba(0,0,0,.18);} +@supports (height:100dvh){.sidebar{height:100dvh;}} .brand-block{height:112px;display:flex;align-items:center;gap:14px;padding:22px 22px;border-bottom:1px solid var(--border);} .brand-mark{width:54px;height:54px;border-radius:18px;display:grid;place-items:center;background:linear-gradient(135deg,rgba(90,73,245,.18),rgba(174,47,243,.18));font-size:2rem;filter:drop-shadow(0 10px 18px rgba(0,0,0,.35));} .brand-copy{display:flex;flex-direction:column;gap:2px;} @@ -181,7 +183,8 @@ body.light-mode{ .avatar-dragon{width:42px;height:42px;border-radius:14px;background:#111;display:grid;place-items:center;font-size:1.45rem;} .sidebar-foot strong{display:block;font-size:.93rem;} .sidebar-foot span{display:block;color:var(--muted);font-size:.78rem;margin-top:2px;text-transform:capitalize;} -.workspace{min-width:0;display:flex;flex-direction:column;background:radial-gradient(circle at 35% -10%,rgba(90,73,245,.12),transparent 32%),var(--bg);} +.workspace{min-width:0;margin-left:280px;display:flex;flex-direction:column;min-height:100vh;background:radial-gradient(circle at 35% -10%,rgba(90,73,245,.12),transparent 32%),var(--bg);} +@supports (min-height:100dvh){.workspace{min-height:100dvh;}} .topbar{height:84px;display:flex;align-items:center;justify-content:space-between;gap:18px;padding:0 26px;border-bottom:1px solid var(--border);background:rgba(31,31,32,.86);backdrop-filter:blur(18px);position:sticky;top:0;z-index:15;margin:0;} body.light-mode .topbar{background:rgba(255,255,255,.82);} .topbar-left,.topbar-actions{display:flex;align-items:center;gap:14px;min-width:0;} @@ -257,9 +260,10 @@ tbody tr:hover{background:rgba(90,73,245,.08);} .badge-on{color:var(--success);font-weight:750}.badge-off{color:var(--muted);font-weight:700} pre.log-box{background:#121214;border-color:var(--border);border-radius:15px;} .drawer-backdrop{display:none;} -@media(max-width:1180px){.dash-grid{grid-template-columns:repeat(2,minmax(0,1fr));}.panel-layout{grid-template-columns:250px minmax(0,1fr);}} +@media(max-width:1180px){.dash-grid{grid-template-columns:repeat(2,minmax(0,1fr));}.sidebar{width:250px;}.workspace{margin-left:250px;}} @media(max-width:820px){ - .panel-layout{grid-template-columns:1fr;} + .panel-layout{display:block;} + .workspace{margin-left:0;} .sidebar{position:fixed;left:0;top:0;bottom:0;width:min(82vw,300px);height:100vh;transform:translateX(-105%);transition:transform .22s ease;} body.sidebar-open .sidebar{transform:translateX(0);} .drawer-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.58);z-index:18;backdrop-filter:blur(4px);} @@ -327,3 +331,7 @@ pre.log-box{background:#121214;border-color:var(--border);border-radius:15px;} .dash-icon{width:54px;height:54px;font-size:1.35rem;} .btn{padding:9px 11px;} } + +/* Xray client table updates in place; this tiny state avoids visual flicker during background polling. */ +#inboundsContainer.xray-refreshing{opacity:.985;} +#inboundsContainer [data-cell]{transition:background-color .12s ease;} diff --git a/admin/assets/app.js b/admin/assets/app.js index 86c0bcd..192c5e5 100644 --- a/admin/assets/app.js +++ b/admin/assets/app.js @@ -10,6 +10,8 @@ let editingXrayClientId = null; let wzInbounds = []; let dashboardCache = { sshUsers: [], xrayInbounds: [], me: null }; let currentTab = "dashboard"; +let inboundsRefreshInFlight = false; +let lastInboundsStructure = ""; // ─── DOM refs ───────────────────────────────────────────────────────────────── const loginOverlay = document.getElementById("loginOverlay"); @@ -194,6 +196,92 @@ function genUUID() { (c^crypto.getRandomValues(new Uint8Array(1))[0]&15>>c/4).toString(16)); } + +function escapeHTML(value) { + return String(value ?? "").replace(/[&<>'"]/g, ch => ({ + "&": "&", "<": "<", ">": ">", "'": "'", '"': """, + }[ch])); +} + +function isVisible(el) { + return !!el && !el.classList.contains("hidden") && getComputedStyle(el).display !== "none"; +} + +function isXrayClientEditorActive() { + const active = document.activeElement; + if (active && active.closest && (active.closest("#inboundsContainer") || active.closest("#editXrayClientPanel"))) return true; + if (isVisible(document.getElementById("editXrayClientPanel"))) return true; + return Array.from(document.querySelectorAll('#inboundsContainer [id^="add-form-"]')).some(isVisible); +} + +function inboundStructure(inbounds = []) { + return JSON.stringify((inbounds || []).map(ib => ({ + tag: ib.tag || "", + protocol: ib.protocol || "", + port: ib.port ?? "", + clients: (ib.clients || []).map(c => c.id || ""), + }))); +} + +function clientStatusHTML(c) { + const exp = c.expires_at ? new Date(c.expires_at) : null; + const daysLeft = c.expiration_days; + if (c.expired) return `Expired`; + if (daysLeft === -1 || !exp) return `Active`; + return `Active (${escapeHTML(daysLeft)}d)`; +} + +function clientExpiryLabel(c) { + const exp = c.expires_at ? new Date(c.expires_at) : null; + return exp ? exp.toLocaleDateString() : "Unlimited"; +} + +function clientOnlineHTML(c) { + return `${c.online ? 'online' : 'offline'}
${escapeHTML(formatLastActive(c.last_active))}
`; +} + +function updateCell(row, name, html) { + const cell = row?.querySelector?.(`[data-cell="${name}"]`); + if (cell && cell.innerHTML !== html) cell.innerHTML = html; +} + +function patchRenderedInbounds(inbounds) { + const sections = Array.from(inboundsContainer.querySelectorAll("[data-inbound-tag]")); + if (sections.length !== inbounds.length) return false; + + for (const ib of inbounds) { + const tag = String(ib.tag || ""); + const section = sections.find(el => el.dataset.inboundTag === tag); + if (!section) return false; + if (section.dataset.inboundProtocol !== String(ib.protocol || "") || section.dataset.inboundPort !== String(ib.port ?? "")) return false; + + const clients = ib.clients || []; + const rows = Array.from(section.querySelectorAll("tr[data-client-id]")); + if (rows.length !== clients.length) return false; + + const onlineCount = clients.filter(c => !!c.online).length; + const chip = section.querySelector('[data-role="inbound-online-chip"]'); + if (chip) { + chip.textContent = `${onlineCount} online`; + chip.classList.toggle("green", onlineCount > 0); + } + + for (const c of clients) { + const row = rows.find(el => el.dataset.clientId === String(c.id || "")); + if (!row) return false; + updateCell(row, "name", escapeHTML(c.name || "—")); + updateCell(row, "uuid", escapeHTML(c.id || "—")); + updateCell(row, "email", escapeHTML(c.email || "—")); + updateCell(row, "expiry", escapeHTML(clientExpiryLabel(c))); + updateCell(row, "status", clientStatusHTML(c)); + updateCell(row, "online", clientOnlineHTML(c)); + updateCell(row, "traffic", escapeHTML(formatBytes(c.total_bytes))); + updateCell(row, "max", escapeHTML(c.max_conns || "∞")); + } + } + return true; +} + // ─── Navigation / shell ────────────────────────────────────────────────────── const tabTitles = { dashboard: ["Painel", "Visão geral"], @@ -223,7 +311,7 @@ function selectTab(tab) { if (tab === "dashboard") refreshDashboard(); if (tab === "xray") { loadXrayStatus(); - loadInbounds(); + loadInbounds({ force: true }); if (currentRole === "superadmin") loadWizardFromConfig(); } if (tab === "stats" && currentRole === "superadmin") loadStats(); @@ -321,11 +409,14 @@ function initAfterLogin() { } else { loadMe(); } - xrayTimer = setInterval(() => { loadXrayStatus(); if (currentTab === "xray") loadInbounds(); }, 7000); + xrayTimer = setInterval(() => { + loadXrayStatus(); + if (currentTab === "xray") loadInbounds({ silent: true }); + }, 7000); loadUsers(); loadXrayStatus(); - loadInbounds(); + loadInbounds({ silent: true }); usersTimer = setInterval(() => loadUsersSilent(), 3000); } @@ -482,7 +573,7 @@ function updateDashboardXray(inbounds = []) { function refreshDashboard() { loadUsersSilent(); - loadInbounds(); + loadInbounds({ silent: true }); loadXrayStatus(); if (currentRole === "superadmin") loadStats(); if (currentRole === "reseller") loadMe(); @@ -651,17 +742,12 @@ 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(); loadInbounds(); }); -document.getElementById("xLoadInboundsBtn").addEventListener("click", loadInbounds); +document.getElementById("xRefreshBtn").addEventListener("click", () => { loadXrayStatus(); loadInbounds({ force: true }); }); +document.getElementById("xLoadInboundsBtn").addEventListener("click", () => loadInbounds({ force: true })); document.getElementById("xLoadCfgBtn").addEventListener("click", loadXrayCfg); document.getElementById("xSaveCfgBtn").addEventListener("click", saveXrayCfg); document.getElementById("xLoadLogsBtn").addEventListener("click", loadXrayLogs); -document.querySelector("[data-tab='xray']")?.addEventListener("click", () => { - loadXrayStatus(); - loadInbounds(); - if (currentRole === "superadmin") loadWizardFromConfig(); -}); async function loadXrayStatus() { try { @@ -709,7 +795,7 @@ async function repairXrayStats() { ? (d.restarted ? "Counters API repaired and Xray restarted." : "Counters API repaired. Restart Xray to apply it.") : "Counters API already looks correct."; setTimeout(loadXrayStatus, 700); - setTimeout(loadInbounds, 1200); + setTimeout(() => loadInbounds({ force: true }), 1200); } catch (e) { if (e.message==="auth") doAuthError(); else xStatus.textContent = "Error repairing counters: "+e.message; @@ -725,22 +811,30 @@ async function xrayCtrl(action) { if (!res.ok) throw new Error(await res.text()); xStatus.textContent = "Xray "+action+" OK."; setTimeout(loadXrayStatus, 700); - setTimeout(loadInbounds, 1200); + setTimeout(() => loadInbounds({ force: true }), 1200); } catch (e) { if (e.message==="auth") doAuthError(); else xStatus.textContent = "Error: "+e.message; } } -async function loadInbounds() { - inboundsContainer.innerHTML = '
Loading…
'; +async function loadInbounds(options = {}) { + const { silent = false, force = false } = options || {}; + if (inboundsRefreshInFlight) return; + inboundsRefreshInFlight = true; + if (!silent) inboundsContainer.innerHTML = '
Loading…
'; + else inboundsContainer.classList.add("xray-refreshing"); try { const res = await api("/api/xray/inbounds"); + if (!res.ok) throw new Error(await res.text()); const inbounds = await res.json(); - renderInbounds(inbounds || []); + renderInbounds(inbounds || [], { silent, force }); } catch (e) { - inboundsContainer.textContent = "Error loading inbounds."; + if (!silent) inboundsContainer.textContent = "Error loading inbounds."; if (e.message==="auth") doAuthError(); + } finally { + inboundsRefreshInFlight = false; + inboundsContainer.classList.remove("xray-refreshing"); } } @@ -762,15 +856,30 @@ async function copyText(text) { finally { document.body.removeChild(ta); } } -function renderInbounds(inbounds) { +function renderInbounds(inbounds, options = {}) { + const { silent = false, force = false } = options || {}; updateDashboardXray(inbounds); + const nextStructure = inboundStructure(inbounds); + + if (silent && !force && nextStructure === lastInboundsStructure && patchRenderedInbounds(inbounds)) return; + if (silent && !force && isXrayClientEditorActive()) { + patchRenderedInbounds(inbounds); + if (xStatus) xStatus.textContent = "New client data is available; editing was preserved."; + return; + } + if (!inbounds.length) { inboundsContainer.innerHTML = '
No VLESS/VMess/Trojan inbounds found.
'; + lastInboundsStructure = nextStructure; return; } inboundsContainer.innerHTML = ""; + lastInboundsStructure = nextStructure; inbounds.forEach(ib => { const section = document.createElement("div"); + section.dataset.inboundTag = String(ib.tag || ""); + section.dataset.inboundProtocol = String(ib.protocol || ""); + section.dataset.inboundPort = String(ib.port ?? ""); section.style = "margin-bottom:14px;"; const hdr = document.createElement("div"); @@ -780,10 +889,10 @@ function renderInbounds(inbounds) { const onlineCount = clients.filter(c => !!c.online).length; hdr.innerHTML = `
- ${ib.protocol} - ${ib.tag || "untagged"} - :${ib.port ?? "?"} - ${onlineCount} online + ${escapeHTML(ib.protocol)} + ${escapeHTML(ib.tag || "untagged")} + :${escapeHTML(ib.port ?? "?")} + ${onlineCount} online
`; section.appendChild(hdr); @@ -823,28 +932,17 @@ function renderInbounds(inbounds) { tbl.innerHTML = `NameUUIDEmailExpiryStatusOnlineTrafficMaxActions`; const tbody = document.createElement("tbody"); clients.forEach(c => { - const exp = c.expires_at ? new Date(c.expires_at) : null; - const expStr = exp ? exp.toLocaleDateString() : "Unlimited"; - const isExpired = !!c.expired; - const daysLeft = c.expiration_days; - let statusHtml; - if (isExpired) { - statusHtml = `Expired`; - } else if (daysLeft === -1 || !exp) { - statusHtml = `Active`; - } else { - statusHtml = `Active (${daysLeft}d)`; - } const tr = document.createElement("tr"); + tr.dataset.clientId = String(c.id || ""); tr.innerHTML = ` - ${c.name || "—"} - ${c.id} - ${c.email || "—"} - ${expStr} - ${statusHtml} - ${c.online ? 'online' : 'offline'}
${formatLastActive(c.last_active)}
- ${formatBytes(c.total_bytes)} - ${c.max_conns || "∞"}`; + ${escapeHTML(c.name || "—")} + ${escapeHTML(c.id || "—")} + ${escapeHTML(c.email || "—")} + ${escapeHTML(clientExpiryLabel(c))} + ${clientStatusHTML(c)} + ${clientOnlineHTML(c)} + ${escapeHTML(formatBytes(c.total_bytes))} + ${escapeHTML(c.max_conns || "∞")}`; const actTd = document.createElement("td"); actTd.style.whiteSpace = "nowrap"; const copyBtn = document.createElement("button"); @@ -904,7 +1002,7 @@ async function addClient(tag) { }); if (!res.ok) throw new Error(await res.text()); xStatus.textContent = `Client ${uuid.slice(0,8)}… added. Restarting Xray…`; - setTimeout(() => { loadInbounds(); if (currentRole === "reseller") loadMe(); }, 1500); + setTimeout(() => { loadInbounds({ force: true }); if (currentRole === "reseller") loadMe(); }, 1500); } catch (e) { if (e.message==="auth") doAuthError(); else xStatus.textContent = "Error: "+e.message; @@ -917,7 +1015,7 @@ async function removeClient(tag, uuid) { const res = await api(`/api/xray/clients/remove?inbound_tag=${encodeURIComponent(tag)}&uuid=${encodeURIComponent(uuid)}`, { method:"DELETE" }); if (!res.ok && res.status !== 204) throw new Error(await res.text()); xStatus.textContent = "Client removed. Restarting Xray…"; - setTimeout(() => { loadInbounds(); if (currentRole === "reseller") loadMe(); }, 1500); + setTimeout(() => { loadInbounds({ force: true }); if (currentRole === "reseller") loadMe(); }, 1500); } catch (e) { if (e.message==="auth") doAuthError(); else xStatus.textContent = "Error: "+e.message; @@ -1639,7 +1737,7 @@ async function saveEditXrayClient() { const res = await api("/api/xray/clients/update", { method:"POST", body: JSON.stringify(payload) }); if (!res.ok) throw new Error(await res.text()); st.textContent = "Saved."; - setTimeout(() => { closeEditXrayClient(); loadInbounds(); }, 700); + setTimeout(() => { closeEditXrayClient(); loadInbounds({ force: true }); }, 700); } catch (e) { if (e.message==="auth") doAuthError(); else st.textContent = "Error: " + e.message; diff --git a/admin/index.html b/admin/index.html index 7cd98f5..9e215fc 100644 --- a/admin/index.html +++ b/admin/index.html @@ -4,7 +4,7 @@ DragonCore Panel - +
@@ -876,6 +876,6 @@
- +