Fix panel
This commit is contained in:
@@ -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;}
|
||||||
|
|||||||
@@ -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 => ({
|
||||||
|
"&": "&", "<": "<", ">": ">", "'": "'", '"': """,
|
||||||
|
}[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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user