Fix panel

This commit is contained in:
2026-05-10 18:21:03 -03:00
parent e00a7bd93c
commit 4a04ff79f0
3 changed files with 158 additions and 52 deletions

View File

@@ -164,8 +164,10 @@ body.light-mode{
} }
.app{padding:0;background:linear-gradient(180deg,var(--bg),#101010);min-height:100vh;} .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;} .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);} .panel-layout{display:block;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);} @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-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-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;} .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;} .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 strong{display:block;font-size:.93rem;}
.sidebar-foot span{display:block;color:var(--muted);font-size:.78rem;margin-top:2px;text-transform:capitalize;} .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;} .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);} body.light-mode .topbar{background:rgba(255,255,255,.82);}
.topbar-left,.topbar-actions{display:flex;align-items:center;gap:14px;min-width:0;} .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} .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;} pre.log-box{background:#121214;border-color:var(--border);border-radius:15px;}
.drawer-backdrop{display:none;} .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){ @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;} .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);} 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);} .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;} .dash-icon{width:54px;height:54px;font-size:1.35rem;}
.btn{padding:9px 11px;} .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;}

View File

@@ -10,6 +10,8 @@ let editingXrayClientId = null;
let wzInbounds = []; let wzInbounds = [];
let dashboardCache = { sshUsers: [], xrayInbounds: [], me: null }; let dashboardCache = { sshUsers: [], xrayInbounds: [], me: null };
let currentTab = "dashboard"; let currentTab = "dashboard";
let inboundsRefreshInFlight = false;
let lastInboundsStructure = "";
// ─── DOM refs ───────────────────────────────────────────────────────────────── // ─── DOM refs ─────────────────────────────────────────────────────────────────
const loginOverlay = document.getElementById("loginOverlay"); const loginOverlay = document.getElementById("loginOverlay");
@@ -194,6 +196,92 @@ function genUUID() {
(c^crypto.getRandomValues(new Uint8Array(1))[0]&15>>c/4).toString(16)); (c^crypto.getRandomValues(new Uint8Array(1))[0]&15>>c/4).toString(16));
} }
function escapeHTML(value) {
return String(value ?? "").replace(/[&<>'"]/g, ch => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;",
}[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 `<span style="color:var(--danger);font-size:.68rem;">Expired</span>`;
if (daysLeft === -1 || !exp) return `<span style="color:var(--success);font-size:.68rem;">Active</span>`;
return `<span style="color:var(--success);font-size:.68rem;">Active (${escapeHTML(daysLeft)}d)</span>`;
}
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 ? '<span class="badge-on">online</span>' : '<span class="badge-off">offline</span>'}<div class="hint">${escapeHTML(formatLastActive(c.last_active))}</div>`;
}
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 ────────────────────────────────────────────────────── // ─── Navigation / shell ──────────────────────────────────────────────────────
const tabTitles = { const tabTitles = {
dashboard: ["Painel", "Visão geral"], dashboard: ["Painel", "Visão geral"],
@@ -223,7 +311,7 @@ function selectTab(tab) {
if (tab === "dashboard") refreshDashboard(); if (tab === "dashboard") refreshDashboard();
if (tab === "xray") { if (tab === "xray") {
loadXrayStatus(); loadXrayStatus();
loadInbounds(); loadInbounds({ force: true });
if (currentRole === "superadmin") loadWizardFromConfig(); if (currentRole === "superadmin") loadWizardFromConfig();
} }
if (tab === "stats" && currentRole === "superadmin") loadStats(); if (tab === "stats" && currentRole === "superadmin") loadStats();
@@ -321,11 +409,14 @@ function initAfterLogin() {
} else { } else {
loadMe(); loadMe();
} }
xrayTimer = setInterval(() => { loadXrayStatus(); if (currentTab === "xray") loadInbounds(); }, 7000); xrayTimer = setInterval(() => {
loadXrayStatus();
if (currentTab === "xray") loadInbounds({ silent: true });
}, 7000);
loadUsers(); loadUsers();
loadXrayStatus(); loadXrayStatus();
loadInbounds(); loadInbounds({ silent: true });
usersTimer = setInterval(() => loadUsersSilent(), 3000); usersTimer = setInterval(() => loadUsersSilent(), 3000);
} }
@@ -482,7 +573,7 @@ function updateDashboardXray(inbounds = []) {
function refreshDashboard() { function refreshDashboard() {
loadUsersSilent(); loadUsersSilent();
loadInbounds(); loadInbounds({ silent: true });
loadXrayStatus(); loadXrayStatus();
if (currentRole === "superadmin") loadStats(); if (currentRole === "superadmin") loadStats();
if (currentRole === "reseller") loadMe(); if (currentRole === "reseller") loadMe();
@@ -651,17 +742,12 @@ document.getElementById("xStartBtn").addEventListener("click", () => xrayCtrl("s
document.getElementById("xStopBtn").addEventListener("click", () => xrayCtrl("stop")); document.getElementById("xStopBtn").addEventListener("click", () => xrayCtrl("stop"));
document.getElementById("xRestartBtn").addEventListener("click", () => xrayCtrl("restart")); document.getElementById("xRestartBtn").addEventListener("click", () => xrayCtrl("restart"));
document.getElementById("xRepairStatsBtn")?.addEventListener("click", repairXrayStats); document.getElementById("xRepairStatsBtn")?.addEventListener("click", repairXrayStats);
document.getElementById("xRefreshBtn").addEventListener("click", () => { loadXrayStatus(); loadInbounds(); }); document.getElementById("xRefreshBtn").addEventListener("click", () => { loadXrayStatus(); loadInbounds({ force: true }); });
document.getElementById("xLoadInboundsBtn").addEventListener("click", loadInbounds); document.getElementById("xLoadInboundsBtn").addEventListener("click", () => loadInbounds({ force: true }));
document.getElementById("xLoadCfgBtn").addEventListener("click", loadXrayCfg); document.getElementById("xLoadCfgBtn").addEventListener("click", loadXrayCfg);
document.getElementById("xSaveCfgBtn").addEventListener("click", saveXrayCfg); document.getElementById("xSaveCfgBtn").addEventListener("click", saveXrayCfg);
document.getElementById("xLoadLogsBtn").addEventListener("click", loadXrayLogs); document.getElementById("xLoadLogsBtn").addEventListener("click", loadXrayLogs);
document.querySelector("[data-tab='xray']")?.addEventListener("click", () => {
loadXrayStatus();
loadInbounds();
if (currentRole === "superadmin") loadWizardFromConfig();
});
async function loadXrayStatus() { async function loadXrayStatus() {
try { try {
@@ -709,7 +795,7 @@ async function repairXrayStats() {
? (d.restarted ? "Counters API repaired and Xray restarted." : "Counters API repaired. Restart Xray to apply it.") ? (d.restarted ? "Counters API repaired and Xray restarted." : "Counters API repaired. Restart Xray to apply it.")
: "Counters API already looks correct."; : "Counters API already looks correct.";
setTimeout(loadXrayStatus, 700); setTimeout(loadXrayStatus, 700);
setTimeout(loadInbounds, 1200); setTimeout(() => loadInbounds({ force: true }), 1200);
} catch (e) { } catch (e) {
if (e.message==="auth") doAuthError(); if (e.message==="auth") doAuthError();
else xStatus.textContent = "Error repairing counters: "+e.message; 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()); if (!res.ok) throw new Error(await res.text());
xStatus.textContent = "Xray "+action+" OK."; xStatus.textContent = "Xray "+action+" OK.";
setTimeout(loadXrayStatus, 700); setTimeout(loadXrayStatus, 700);
setTimeout(loadInbounds, 1200); setTimeout(() => loadInbounds({ force: true }), 1200);
} catch (e) { } catch (e) {
if (e.message==="auth") doAuthError(); if (e.message==="auth") doAuthError();
else xStatus.textContent = "Error: "+e.message; else xStatus.textContent = "Error: "+e.message;
} }
} }
async function loadInbounds() { async function loadInbounds(options = {}) {
inboundsContainer.innerHTML = '<div class="hint" style="padding:8px 0;">Loading…</div>'; const { silent = false, force = false } = options || {};
if (inboundsRefreshInFlight) return;
inboundsRefreshInFlight = true;
if (!silent) inboundsContainer.innerHTML = '<div class="hint" style="padding:8px 0;">Loading…</div>';
else inboundsContainer.classList.add("xray-refreshing");
try { try {
const res = await api("/api/xray/inbounds"); const res = await api("/api/xray/inbounds");
if (!res.ok) throw new Error(await res.text());
const inbounds = await res.json(); const inbounds = await res.json();
renderInbounds(inbounds || []); renderInbounds(inbounds || [], { silent, force });
} catch (e) { } catch (e) {
inboundsContainer.textContent = "Error loading inbounds."; if (!silent) inboundsContainer.textContent = "Error loading inbounds.";
if (e.message==="auth") doAuthError(); 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); } finally { document.body.removeChild(ta); }
} }
function renderInbounds(inbounds) { function renderInbounds(inbounds, options = {}) {
const { silent = false, force = false } = options || {};
updateDashboardXray(inbounds); 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) { if (!inbounds.length) {
inboundsContainer.innerHTML = '<div class="hint" style="padding:8px 0;">No VLESS/VMess/Trojan inbounds found.</div>'; inboundsContainer.innerHTML = '<div class="hint" style="padding:8px 0;">No VLESS/VMess/Trojan inbounds found.</div>';
lastInboundsStructure = nextStructure;
return; return;
} }
inboundsContainer.innerHTML = ""; inboundsContainer.innerHTML = "";
lastInboundsStructure = nextStructure;
inbounds.forEach(ib => { inbounds.forEach(ib => {
const section = document.createElement("div"); 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;"; section.style = "margin-bottom:14px;";
const hdr = document.createElement("div"); const hdr = document.createElement("div");
@@ -780,10 +889,10 @@ function renderInbounds(inbounds) {
const onlineCount = clients.filter(c => !!c.online).length; const onlineCount = clients.filter(c => !!c.online).length;
hdr.innerHTML = ` hdr.innerHTML = `
<div class="card-title" style="font-size:.8rem;"> <div class="card-title" style="font-size:.8rem;">
<span class="chip">${ib.protocol}</span> <span class="chip">${escapeHTML(ib.protocol)}</span>
${ib.tag || "untagged"} ${escapeHTML(ib.tag || "untagged")}
<span class="hint">:${ib.port ?? "?"}</span> <span class="hint">:${escapeHTML(ib.port ?? "?")}</span>
<span class="chip ${onlineCount ? "green" : ""}">${onlineCount} online</span> <span class="chip ${onlineCount ? "green" : ""}" data-role="inbound-online-chip">${onlineCount} online</span>
</div> </div>
<button class="btn btn-sm" onclick="openAddClient('${ib.tag}')">+ Add Client</button>`; <button class="btn btn-sm" onclick="openAddClient('${ib.tag}')">+ Add Client</button>`;
section.appendChild(hdr); section.appendChild(hdr);
@@ -823,28 +932,17 @@ function renderInbounds(inbounds) {
tbl.innerHTML = `<thead><tr><th>Name</th><th>UUID</th><th>Email</th><th>Expiry</th><th>Status</th><th>Online</th><th>Traffic</th><th>Max</th><th>Actions</th></tr></thead>`; tbl.innerHTML = `<thead><tr><th>Name</th><th>UUID</th><th>Email</th><th>Expiry</th><th>Status</th><th>Online</th><th>Traffic</th><th>Max</th><th>Actions</th></tr></thead>`;
const tbody = document.createElement("tbody"); const tbody = document.createElement("tbody");
clients.forEach(c => { 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 = `<span style="color:var(--danger);font-size:.68rem;">Expired</span>`;
} else if (daysLeft === -1 || !exp) {
statusHtml = `<span style="color:var(--success);font-size:.68rem;">Active</span>`;
} else {
statusHtml = `<span style="color:var(--success);font-size:.68rem;">Active (${daysLeft}d)</span>`;
}
const tr = document.createElement("tr"); const tr = document.createElement("tr");
tr.dataset.clientId = String(c.id || "");
tr.innerHTML = ` tr.innerHTML = `
<td>${c.name || "—"}</td> <td data-cell="name">${escapeHTML(c.name || "—")}</td>
<td style="font-family:monospace;font-size:.65rem;">${c.id}</td> <td data-cell="uuid" style="font-family:monospace;font-size:.65rem;">${escapeHTML(c.id || "—")}</td>
<td>${c.email || "—"}</td> <td data-cell="email">${escapeHTML(c.email || "—")}</td>
<td style="font-size:.7rem;">${expStr}</td> <td data-cell="expiry" style="font-size:.7rem;">${escapeHTML(clientExpiryLabel(c))}</td>
<td>${statusHtml}</td> <td data-cell="status">${clientStatusHTML(c)}</td>
<td>${c.online ? '<span class="badge-on">online</span>' : '<span class="badge-off">offline</span>'}<div class="hint">${formatLastActive(c.last_active)}</div></td> <td data-cell="online">${clientOnlineHTML(c)}</td>
<td style="font-size:.7rem;">${formatBytes(c.total_bytes)}</td> <td data-cell="traffic" style="font-size:.7rem;">${escapeHTML(formatBytes(c.total_bytes))}</td>
<td style="font-size:.7rem;">${c.max_conns || "∞"}</td>`; <td data-cell="max" style="font-size:.7rem;">${escapeHTML(c.max_conns || "∞")}</td>`;
const actTd = document.createElement("td"); const actTd = document.createElement("td");
actTd.style.whiteSpace = "nowrap"; actTd.style.whiteSpace = "nowrap";
const copyBtn = document.createElement("button"); const copyBtn = document.createElement("button");
@@ -904,7 +1002,7 @@ async function addClient(tag) {
}); });
if (!res.ok) throw new Error(await res.text()); if (!res.ok) throw new Error(await res.text());
xStatus.textContent = `Client ${uuid.slice(0,8)}… added. Restarting Xray…`; 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) { } catch (e) {
if (e.message==="auth") doAuthError(); if (e.message==="auth") doAuthError();
else xStatus.textContent = "Error: "+e.message; 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" }); 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()); if (!res.ok && res.status !== 204) throw new Error(await res.text());
xStatus.textContent = "Client removed. Restarting Xray…"; xStatus.textContent = "Client removed. Restarting Xray…";
setTimeout(() => { loadInbounds(); if (currentRole === "reseller") loadMe(); }, 1500); setTimeout(() => { loadInbounds({ force: true }); if (currentRole === "reseller") loadMe(); }, 1500);
} catch (e) { } catch (e) {
if (e.message==="auth") doAuthError(); if (e.message==="auth") doAuthError();
else xStatus.textContent = "Error: "+e.message; 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) }); const res = await api("/api/xray/clients/update", { method:"POST", body: JSON.stringify(payload) });
if (!res.ok) throw new Error(await res.text()); if (!res.ok) throw new Error(await res.text());
st.textContent = "Saved."; st.textContent = "Saved.";
setTimeout(() => { closeEditXrayClient(); loadInbounds(); }, 700); setTimeout(() => { closeEditXrayClient(); 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 st.textContent = "Error: " + e.message;

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<title>DragonCore Panel</title> <title>DragonCore Panel</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/> <meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="assets/app.css?v=20260510xraystats2"/> <link rel="stylesheet" href="assets/app.css?v=20260510xraystable1"/>
</head> </head>
<body> <body>
<div class="app"> <div class="app">
@@ -876,6 +876,6 @@
</div><!-- /shell --> </div><!-- /shell -->
</div><!-- /app --> </div><!-- /app -->
<script defer src="assets/app.js?v=20260510xraystats2"></script> <script defer src="assets/app.js?v=20260510xraystable1"></script>
</body> </body>
</html> </html>