Fix ssh list
This commit is contained in:
@@ -15,7 +15,7 @@ DragonCoreSSH V40 é um painel/servidor em Go para SSH com HTTP Injection, paine
|
|||||||
- Endpoint XHTTP compartilhado no modo nativo: VLESS **ou** VMess em `/` e SSH em `/ssh`, usando o mesmo domínio/porta/TLS
|
- Endpoint XHTTP compartilhado no modo nativo: VLESS **ou** VMess em `/` e SSH em `/ssh`, usando o mesmo domínio/porta/TLS
|
||||||
- Área compacta de infraestrutura com Servidores, Status, Monitoramento e Tráfego no mesmo seletor visual
|
- Área compacta de infraestrutura com Servidores, Status, Monitoramento e Tráfego no mesmo seletor visual
|
||||||
- Cartões de status ao vivo nos espaços SSH, Xray e Infraestrutura, com confirmações integradas ao painel
|
- Cartões de status ao vivo nos espaços SSH, Xray e Infraestrutura, com confirmações integradas ao painel
|
||||||
- Lista de usuários Xray com botões de ordenação e filtros por status, conexão, uso, validade e cota, além de cabeçalhos clicáveis como na lista SSH
|
- Listas de usuários SSH e Xray com botões de ordenação e filtros por status, conexão, uso, validade e cota, além de cabeçalhos clicáveis
|
||||||
- Navegação interna consistente com o Bot: SSH/SlowDNS e Revendedores separam consulta de cadastro; Xray separa Usuários, Criar usuário, Configuração e Logs; Configurações separa Rede/SSH, SlowDNS, UDP, TLS e Xray
|
- Navegação interna consistente com o Bot: SSH/SlowDNS e Revendedores separam consulta de cadastro; Xray separa Usuários, Criar usuário, Configuração e Logs; Configurações separa Rede/SSH, SlowDNS, UDP, TLS e Xray
|
||||||
- Contas de revendedor (reseller) com cota de usuários e escopo próprio
|
- Contas de revendedor (reseller) com cota de usuários e escopo próprio
|
||||||
- Gerenciamento multi-servidor (master/slave) direto pelo painel
|
- Gerenciamento multi-servidor (master/slave) direto pelo painel
|
||||||
|
|||||||
@@ -758,7 +758,7 @@ th[data-sort-key]::after{content:"";display:inline-block;width:.9em;font-size:.7
|
|||||||
th[data-sort-key].sort-asc::after{content:" \25B2";}
|
th[data-sort-key].sort-asc::after{content:" \25B2";}
|
||||||
th[data-sort-key].sort-desc::after{content:" \25BC";}
|
th[data-sort-key].sort-desc::after{content:" \25BC";}
|
||||||
|
|
||||||
/* Xray user list sorting and filtering */
|
/* Shared SSH/Xray user list sorting and filtering */
|
||||||
.xray-list-controls{display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;margin:0 0 16px;padding:12px;border:1px solid rgba(139,92,246,.18);border-radius:16px;background:rgba(139,92,246,.055)}
|
.user-list-controls{display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;margin:0 0 16px;padding:12px;border:1px solid rgba(var(--section-accent,139,92,246),.18);border-radius:16px;background:rgba(var(--section-accent,139,92,246),.055)}
|
||||||
.xray-list-control-group{display:flex;flex-direction:column;gap:6px;min-width:0}.xray-list-control-label{color:var(--muted);font-size:.63rem;font-weight:900;letter-spacing:.09em;text-transform:uppercase}.xray-list-buttons{display:flex;align-items:center;gap:5px;flex-wrap:wrap}.xray-list-filter-btn{min-height:30px;padding:5px 9px;border:1px solid rgba(148,163,184,.16);border-radius:10px;background:rgba(255,255,255,.025);color:var(--muted);font-size:.68rem;font-weight:850;cursor:pointer;transition:.15s ease}.xray-list-filter-btn:hover{color:var(--text);border-color:rgba(139,92,246,.38);background:rgba(139,92,246,.09)}.xray-list-filter-btn.active{color:#fff;border-color:rgba(139,92,246,.44);background:linear-gradient(135deg,rgba(139,92,246,.3),rgba(34,211,238,.1));box-shadow:inset 0 1px 0 rgba(255,255,255,.06)}.xray-list-filter-btn[data-direction]::after{margin-left:4px;font-size:.7em}.xray-list-filter-btn[data-direction="asc"]::after{content:"\25B2"}.xray-list-filter-btn[data-direction="desc"]::after{content:"\25BC"}.xray-list-count{margin-left:auto;white-space:nowrap}
|
.user-list-control-group{display:flex;flex-direction:column;gap:6px;min-width:0}.user-list-control-label{color:var(--muted);font-size:.63rem;font-weight:900;letter-spacing:.09em;text-transform:uppercase}.user-list-buttons{display:flex;align-items:center;gap:5px;flex-wrap:wrap}.user-list-filter-btn{min-height:30px;padding:5px 9px;border:1px solid rgba(148,163,184,.16);border-radius:10px;background:rgba(255,255,255,.025);color:var(--muted);font-size:.68rem;font-weight:850;cursor:pointer;transition:.15s ease}.user-list-filter-btn:hover{color:var(--text);border-color:rgba(var(--section-accent,139,92,246),.38);background:rgba(var(--section-accent,139,92,246),.09)}.user-list-filter-btn.active{color:#fff;border-color:rgba(var(--section-accent,139,92,246),.44);background:linear-gradient(135deg,rgba(var(--section-accent,139,92,246),.3),rgba(34,211,238,.1));box-shadow:inset 0 1px 0 rgba(255,255,255,.06)}.user-list-filter-btn[data-direction]::after{margin-left:4px;font-size:.7em}.user-list-filter-btn[data-direction="asc"]::after{content:"\25B2"}.user-list-filter-btn[data-direction="desc"]::after{content:"\25BC"}.user-list-count{margin-left:auto;white-space:nowrap}
|
||||||
@media(max-width:760px){.xray-list-controls{align-items:stretch}.xray-list-control-group{width:100%}.xray-list-buttons{display:grid;grid-template-columns:repeat(3,minmax(0,1fr))}.xray-list-filter-btn{width:100%}.xray-list-count{margin-left:0;align-self:flex-start}}
|
@media(max-width:760px){.user-list-controls{align-items:stretch}.user-list-control-group{width:100%}.user-list-buttons{display:grid;grid-template-columns:repeat(3,minmax(0,1fr))}.user-list-filter-btn{width:100%}.user-list-count{margin-left:0;align-self:flex-start}}
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ function applyLanguage(lang, options = {}) {
|
|||||||
if (languageSelect) languageSelect.value = currentLang;
|
if (languageSelect) languageSelect.value = currentLang;
|
||||||
updatePageHeading();
|
updatePageHeading();
|
||||||
translateStatic(document.body);
|
translateStatic(document.body);
|
||||||
|
if (typeof window.renderSSHListControls === "function") window.renderSSHListControls();
|
||||||
if (typeof window.renderXrayListControls === "function") window.renderXrayListControls();
|
if (typeof window.renderXrayListControls === "function") window.renderXrayListControls();
|
||||||
document.documentElement.classList.remove("i18n-pending");
|
document.documentElement.classList.remove("i18n-pending");
|
||||||
}
|
}
|
||||||
@@ -756,7 +757,7 @@ function patchRenderedInbounds(inbounds) {
|
|||||||
// Native XHTTP/VPN tuning labels introduced by the high-traffic backpressure
|
// Native XHTTP/VPN tuning labels introduced by the high-traffic backpressure
|
||||||
// update. Keep this block close to the UI code so both languages stay complete.
|
// update. Keep this block close to the UI code so both languages stay complete.
|
||||||
Object.assign(I18N_TEXT["en-US"], {
|
Object.assign(I18N_TEXT["en-US"], {
|
||||||
"Sort by":"Sort by","Show":"Show","All":"All","Offline":"Offline","Connections":"Connections","Usage":"Usage","Quota reached":"Quota reached","{visible} of {total} users":"{visible} of {total} users","No Xray users match this filter.":"No Xray users match this filter.",
|
"Sort by":"Sort by","Show":"Show","All":"All","Offline":"Offline","Connections":"Connections","Usage":"Usage","Quota reached":"Quota reached","{visible} of {total} users":"{visible} of {total} users","No Xray users match this filter.":"No Xray users match this filter.","No SSH users match this filter.":"No SSH users match this filter.",
|
||||||
"Native Xray scale tuning":"Native Xray scale tuning",
|
"Native Xray scale tuning":"Native Xray scale tuning",
|
||||||
"Go CPU threads (GOMAXPROCS)":"Go CPU threads (GOMAXPROCS)",
|
"Go CPU threads (GOMAXPROCS)":"Go CPU threads (GOMAXPROCS)",
|
||||||
"Global mux backend sessions":"Global mux backend sessions",
|
"Global mux backend sessions":"Global mux backend sessions",
|
||||||
@@ -771,7 +772,7 @@ Object.assign(I18N_TEXT["en-US"], {
|
|||||||
"XHTTP is handled as VPN tunnel traffic: packet requests use bounded backpressure and are never rejected by an HTTP request-rate ceiling. At the transport ceiling, new sockets wait in the kernel backlog instead of being reset. Keep the safe defaults unless the server is sized and load-tested for the high-traffic profile. HTTP/2 retains a 1024-stream flow-control guard per connection, while upload memory stays globally bounded. Saved in the panel config and applied live on restart/reload.":"XHTTP is handled as VPN tunnel traffic: packet requests use bounded backpressure and are never rejected by an HTTP request-rate ceiling. At the transport ceiling, new sockets wait in the kernel backlog instead of being reset. Keep the safe defaults unless the server is sized and load-tested for the high-traffic profile. HTTP/2 retains a 1024-stream flow-control guard per connection, while upload memory stays globally bounded. Saved in the panel config and applied live on restart/reload."
|
"XHTTP is handled as VPN tunnel traffic: packet requests use bounded backpressure and are never rejected by an HTTP request-rate ceiling. At the transport ceiling, new sockets wait in the kernel backlog instead of being reset. Keep the safe defaults unless the server is sized and load-tested for the high-traffic profile. HTTP/2 retains a 1024-stream flow-control guard per connection, while upload memory stays globally bounded. Saved in the panel config and applied live on restart/reload.":"XHTTP is handled as VPN tunnel traffic: packet requests use bounded backpressure and are never rejected by an HTTP request-rate ceiling. At the transport ceiling, new sockets wait in the kernel backlog instead of being reset. Keep the safe defaults unless the server is sized and load-tested for the high-traffic profile. HTTP/2 retains a 1024-stream flow-control guard per connection, while upload memory stays globally bounded. Saved in the panel config and applied live on restart/reload."
|
||||||
});
|
});
|
||||||
Object.assign(I18N_TEXT["pt-BR"], {
|
Object.assign(I18N_TEXT["pt-BR"], {
|
||||||
"Sort by":"Ordenar por","Show":"Mostrar","All":"Todos","Offline":"Offline","Connections":"Conexões","Usage":"Uso","Quota reached":"Cota atingida","{visible} of {total} users":"{visible} de {total} usuários","No Xray users match this filter.":"Nenhum usuário Xray corresponde a este filtro.",
|
"Sort by":"Ordenar por","Show":"Mostrar","All":"Todos","Offline":"Offline","Connections":"Conexões","Usage":"Uso","Quota reached":"Cota atingida","{visible} of {total} users":"{visible} de {total} usuários","No Xray users match this filter.":"Nenhum usuário Xray corresponde a este filtro.","No SSH users match this filter.":"Nenhum usuário SSH corresponde a este filtro.",
|
||||||
"Native Xray scale tuning":"Ajustes de escala do Xray nativo",
|
"Native Xray scale tuning":"Ajustes de escala do Xray nativo",
|
||||||
"Go CPU threads (GOMAXPROCS)":"Threads de CPU do Go (GOMAXPROCS)",
|
"Go CPU threads (GOMAXPROCS)":"Threads de CPU do Go (GOMAXPROCS)",
|
||||||
"Global mux backend sessions":"Sessões globais de backend Mux",
|
"Global mux backend sessions":"Sessões globais de backend Mux",
|
||||||
|
|||||||
@@ -81,10 +81,33 @@ const USER_SORT_EXTRACT = {
|
|||||||
};
|
};
|
||||||
// Columns that default to descending on first click (most/online first).
|
// Columns that default to descending on first click (most/online first).
|
||||||
const USER_SORT_DEFAULT_DESC = new Set(["status", "conn", "max", "up", "down", "usage"]);
|
const USER_SORT_DEFAULT_DESC = new Set(["status", "conn", "max", "up", "down", "usage"]);
|
||||||
|
const USER_SORT_OPTIONS = [
|
||||||
|
["username", "User"], ["status", "Status"], ["auth", "Auth"], ["conn", "Connections"],
|
||||||
|
["usage", "Usage"], ["expires", "Expiry"], ["max", "Max"], ["up", "Up"], ["down", "Dn"],
|
||||||
|
["owner", "Owner"],
|
||||||
|
];
|
||||||
|
const USER_FILTER_OPTIONS = [
|
||||||
|
["all", "All"], ["online", "Online"], ["offline", "Offline"],
|
||||||
|
["active", "Active"], ["expired", "Expired"], ["quota", "Quota reached"],
|
||||||
|
];
|
||||||
|
|
||||||
let userSort = { key: "username", dir: "asc" };
|
let userSort = { key: "username", dir: "asc" };
|
||||||
|
let userFilter = "all";
|
||||||
let lastUsersData = [];
|
let lastUsersData = [];
|
||||||
|
|
||||||
|
function userMatchesFilter(user) {
|
||||||
|
const online = Number(user.active_conns || 0) > 0;
|
||||||
|
const expired = isExpiredDate(user.expires_at);
|
||||||
|
switch (userFilter) {
|
||||||
|
case "online": return online;
|
||||||
|
case "offline": return !online;
|
||||||
|
case "active": return !expired;
|
||||||
|
case "expired": return expired;
|
||||||
|
case "quota": return !!user.quota_exceeded;
|
||||||
|
default: return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sortUsers(list) {
|
function sortUsers(list) {
|
||||||
const ext = USER_SORT_EXTRACT[userSort.key] || USER_SORT_EXTRACT.username;
|
const ext = USER_SORT_EXTRACT[userSort.key] || USER_SORT_EXTRACT.username;
|
||||||
const dir = userSort.dir === "desc" ? -1 : 1;
|
const dir = userSort.dir === "desc" ? -1 : 1;
|
||||||
@@ -110,6 +133,44 @@ function updateSortIndicators() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSSHListControls() {
|
||||||
|
const sortLabel = document.getElementById("sshSortLabel");
|
||||||
|
const filterLabel = document.getElementById("sshFilterLabel");
|
||||||
|
const sortButtons = document.getElementById("sshSortButtons");
|
||||||
|
const filterButtons = document.getElementById("sshFilterButtons");
|
||||||
|
const count = document.getElementById("sshListCount");
|
||||||
|
if (sortLabel) sortLabel.textContent = t("Sort by");
|
||||||
|
if (filterLabel) filterLabel.textContent = t("Show");
|
||||||
|
if (sortButtons) {
|
||||||
|
const options = USER_SORT_OPTIONS.filter(([key]) => key !== "owner" || currentRole === "superadmin");
|
||||||
|
sortButtons.replaceChildren(...options.map(([key, label]) => {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "user-list-filter-btn" + (userSort.key === key ? " active" : "");
|
||||||
|
button.textContent = t(label);
|
||||||
|
button.setAttribute("aria-pressed", userSort.key === key ? "true" : "false");
|
||||||
|
if (userSort.key === key) button.dataset.direction = userSort.dir;
|
||||||
|
button.addEventListener("click", () => setUserSort(key));
|
||||||
|
return button;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (filterButtons) {
|
||||||
|
filterButtons.replaceChildren(...USER_FILTER_OPTIONS.map(([key, label]) => {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "user-list-filter-btn" + (userFilter === key ? " active" : "");
|
||||||
|
button.textContent = t(label);
|
||||||
|
button.setAttribute("aria-pressed", userFilter === key ? "true" : "false");
|
||||||
|
button.addEventListener("click", () => setUserFilter(key));
|
||||||
|
return button;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (count) {
|
||||||
|
const visible = lastUsersData.filter(userMatchesFilter).length;
|
||||||
|
count.textContent = t("{visible} of {total} users", {visible, total:lastUsersData.length});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setUserSort(key) {
|
function setUserSort(key) {
|
||||||
if (!USER_SORT_EXTRACT[key]) return;
|
if (!USER_SORT_EXTRACT[key]) return;
|
||||||
if (userSort.key === key) {
|
if (userSort.key === key) {
|
||||||
@@ -118,7 +179,12 @@ function setUserSort(key) {
|
|||||||
userSort.key = key;
|
userSort.key = key;
|
||||||
userSort.dir = USER_SORT_DEFAULT_DESC.has(key) ? "desc" : "asc";
|
userSort.dir = USER_SORT_DEFAULT_DESC.has(key) ? "desc" : "asc";
|
||||||
}
|
}
|
||||||
updateSortIndicators();
|
renderUsers(lastUsersData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUserFilter(filter) {
|
||||||
|
if (!USER_FILTER_OPTIONS.some(([key]) => key === filter)) return;
|
||||||
|
userFilter = filter;
|
||||||
renderUsers(lastUsersData);
|
renderUsers(lastUsersData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +197,8 @@ function setUserSort(key) {
|
|||||||
updateSortIndicators();
|
updateSortIndicators();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
renderSSHListControls();
|
||||||
|
|
||||||
function sshTrafficHTML(u) {
|
function sshTrafficHTML(u) {
|
||||||
const up = Number(u.total_uplink_bytes || 0);
|
const up = Number(u.total_uplink_bytes || 0);
|
||||||
const down = Number(u.total_downlink_bytes || 0);
|
const down = Number(u.total_downlink_bytes || 0);
|
||||||
@@ -146,19 +214,18 @@ function sshTrafficHTML(u) {
|
|||||||
function renderUsers(users) {
|
function renderUsers(users) {
|
||||||
// Cache the raw list so a header click can re-sort without refetching, and
|
// Cache the raw list so a header click can re-sort without refetching, and
|
||||||
// order by the active column so rows don't shuffle on each live poll.
|
// order by the active column so rows don't shuffle on each live poll.
|
||||||
lastUsersData = users || [];
|
lastUsersData = Array.isArray(users) ? users : [];
|
||||||
users = sortUsers(lastUsersData);
|
users = sortUsers(lastUsersData.filter(userMatchesFilter));
|
||||||
updateDashboardFromUsers(users);
|
updateDashboardFromUsers(lastUsersData);
|
||||||
|
renderSSHListControls();
|
||||||
|
updateSortIndicators();
|
||||||
const isSA = currentRole === "superadmin";
|
const isSA = currentRole === "superadmin";
|
||||||
userCountChip.textContent = users.length;
|
ownerColHead.classList.toggle("hidden", !isSA);
|
||||||
if (isSA) ownerColHead.classList.remove("hidden");
|
|
||||||
usersBody.innerHTML = "";
|
usersBody.innerHTML = "";
|
||||||
let online = 0;
|
const online = lastUsersData.filter(u => Number(u.active_conns || 0) > 0).length;
|
||||||
let expiredCount = 0;
|
const expiredCount = lastUsersData.filter(u => isExpiredDate(u.expires_at)).length;
|
||||||
users.forEach(u => {
|
users.forEach(u => {
|
||||||
const on = (u.active_conns || 0) > 0;
|
const on = (u.active_conns || 0) > 0;
|
||||||
if (on) online++;
|
|
||||||
if (isExpiredDate(u.expires_at)) expiredCount++;
|
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
const cells = [
|
const cells = [
|
||||||
u.username,
|
u.username,
|
||||||
@@ -198,9 +265,19 @@ function renderUsers(users) {
|
|||||||
tr.appendChild(tdA);
|
tr.appendChild(tdA);
|
||||||
usersBody.appendChild(tr);
|
usersBody.appendChild(tr);
|
||||||
});
|
});
|
||||||
const activeCount = Math.max(0, users.length - expiredCount);
|
if (!users.length) {
|
||||||
userCountChip.textContent = t("{count} total · {active} active · {online} online", {count: users.length, active: activeCount, online});
|
const row = document.createElement("tr");
|
||||||
if (sshMetricTotal) sshMetricTotal.textContent = String(users.length);
|
const cell = document.createElement("td");
|
||||||
|
cell.colSpan = isSA ? 11 : 10;
|
||||||
|
cell.className = "hint";
|
||||||
|
cell.style.cssText = "padding:24px;text-align:center;";
|
||||||
|
cell.textContent = t("No SSH users match this filter.");
|
||||||
|
row.appendChild(cell);
|
||||||
|
usersBody.appendChild(row);
|
||||||
|
}
|
||||||
|
const activeCount = Math.max(0, lastUsersData.length - expiredCount);
|
||||||
|
userCountChip.textContent = t("{count} total · {active} active · {online} online", {count:lastUsersData.length, active:activeCount, online});
|
||||||
|
if (sshMetricTotal) sshMetricTotal.textContent = String(lastUsersData.length);
|
||||||
if (sshMetricActive) sshMetricActive.textContent = String(activeCount);
|
if (sshMetricActive) sshMetricActive.textContent = String(activeCount);
|
||||||
if (sshMetricOnline) sshMetricOnline.textContent = String(online);
|
if (sshMetricOnline) sshMetricOnline.textContent = String(online);
|
||||||
if (sshMetricState) sshMetricState.textContent = t("Online");
|
if (sshMetricState) sshMetricState.textContent = t("Online");
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ function renderXrayListControls() {
|
|||||||
sortButtons.replaceChildren(...XRAY_CLIENT_SORT_OPTIONS.map(([key, label]) => {
|
sortButtons.replaceChildren(...XRAY_CLIENT_SORT_OPTIONS.map(([key, label]) => {
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.className = "xray-list-filter-btn" + (xrayClientSort.key === key ? " active" : "");
|
button.className = "user-list-filter-btn" + (xrayClientSort.key === key ? " active" : "");
|
||||||
button.textContent = t(label);
|
button.textContent = t(label);
|
||||||
button.setAttribute("aria-pressed", xrayClientSort.key === key ? "true" : "false");
|
button.setAttribute("aria-pressed", xrayClientSort.key === key ? "true" : "false");
|
||||||
if (xrayClientSort.key === key) button.dataset.direction = xrayClientSort.dir;
|
if (xrayClientSort.key === key) button.dataset.direction = xrayClientSort.dir;
|
||||||
@@ -103,7 +103,7 @@ function renderXrayListControls() {
|
|||||||
filterButtons.replaceChildren(...XRAY_CLIENT_FILTER_OPTIONS.map(([key, label]) => {
|
filterButtons.replaceChildren(...XRAY_CLIENT_FILTER_OPTIONS.map(([key, label]) => {
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.className = "xray-list-filter-btn" + (xrayClientFilter === key ? " active" : "");
|
button.className = "user-list-filter-btn" + (xrayClientFilter === key ? " active" : "");
|
||||||
button.textContent = t(label);
|
button.textContent = t(label);
|
||||||
button.setAttribute("aria-pressed", xrayClientFilter === key ? "true" : "false");
|
button.setAttribute("aria-pressed", xrayClientFilter === key ? "true" : "false");
|
||||||
button.addEventListener("click", () => setXrayClientFilter(key));
|
button.addEventListener("click", () => setXrayClientFilter(key));
|
||||||
|
|||||||
+23
-12
@@ -16,7 +16,7 @@
|
|||||||
setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500);
|
setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="assets/app.css?v=20260720xraylist1"/>
|
<link rel="stylesheet" href="assets/app.css?v=20260720sshfilters1"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
@@ -269,6 +269,17 @@
|
|||||||
<button class="btn btn-ghost btn-sm" id="reloadUsersBtn">Reload</button>
|
<button class="btn btn-ghost btn-sm" id="reloadUsersBtn">Reload</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="user-list-controls" id="sshListControls" aria-label="SSH user list controls">
|
||||||
|
<div class="user-list-control-group">
|
||||||
|
<span class="user-list-control-label" id="sshSortLabel"></span>
|
||||||
|
<div class="user-list-buttons" id="sshSortButtons"></div>
|
||||||
|
</div>
|
||||||
|
<div class="user-list-control-group">
|
||||||
|
<span class="user-list-control-label" id="sshFilterLabel"></span>
|
||||||
|
<div class="user-list-buttons" id="sshFilterButtons"></div>
|
||||||
|
</div>
|
||||||
|
<span class="chip user-list-count" id="sshListCount"></span>
|
||||||
|
</div>
|
||||||
<div class="tbl-wrap">
|
<div class="tbl-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
@@ -420,16 +431,16 @@
|
|||||||
<div class="card-title">Inbounds & Clients</div>
|
<div class="card-title">Inbounds & Clients</div>
|
||||||
<div class="card-actions"><button class="btn btn-ghost btn-sm" id="xLoadInboundsBtn">Reload</button></div>
|
<div class="card-actions"><button class="btn btn-ghost btn-sm" id="xLoadInboundsBtn">Reload</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="xray-list-controls" id="xrayListControls" aria-label="Xray user list controls">
|
<div class="user-list-controls" id="xrayListControls" aria-label="Xray user list controls">
|
||||||
<div class="xray-list-control-group">
|
<div class="user-list-control-group">
|
||||||
<span class="xray-list-control-label" id="xraySortLabel"></span>
|
<span class="user-list-control-label" id="xraySortLabel"></span>
|
||||||
<div class="xray-list-buttons" id="xraySortButtons"></div>
|
<div class="user-list-buttons" id="xraySortButtons"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="xray-list-control-group">
|
<div class="user-list-control-group">
|
||||||
<span class="xray-list-control-label" id="xrayFilterLabel"></span>
|
<span class="user-list-control-label" id="xrayFilterLabel"></span>
|
||||||
<div class="xray-list-buttons" id="xrayFilterButtons"></div>
|
<div class="user-list-buttons" id="xrayFilterButtons"></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="chip xray-list-count" id="xrayListCount"></span>
|
<span class="chip user-list-count" id="xrayListCount"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="inboundsContainer">
|
<div id="inboundsContainer">
|
||||||
<div class="hint" style="padding:8px 0;">Loading inbounds…</div>
|
<div class="hint" style="padding:8px 0;">Loading inbounds…</div>
|
||||||
@@ -1569,10 +1580,10 @@
|
|||||||
<!-- app.js was split into ordered modules for maintainability. They are plain
|
<!-- app.js was split into ordered modules for maintainability. They are plain
|
||||||
classic scripts sharing one global scope; `defer` preserves execution order,
|
classic scripts sharing one global scope; `defer` preserves execution order,
|
||||||
so behavior is identical to the old single file. Keep this load order. -->
|
so behavior is identical to the old single file. Keep this load order. -->
|
||||||
<script defer src="assets/js/01-core.js?v=20260720xraylist1"></script>
|
<script defer src="assets/js/01-core.js?v=20260720sshfilters1"></script>
|
||||||
<script defer src="assets/js/02-shell.js?v=20260714pamfix1"></script>
|
<script defer src="assets/js/02-shell.js?v=20260714pamfix1"></script>
|
||||||
<script defer src="assets/js/03-ssh-users.js?v=20260719quotaaudit1"></script>
|
<script defer src="assets/js/03-ssh-users.js?v=20260720sshfilters1"></script>
|
||||||
<script defer src="assets/js/04-xray.js?v=20260720xraylist1"></script>
|
<script defer src="assets/js/04-xray.js?v=20260720sshfilters1"></script>
|
||||||
<script defer src="assets/js/05-resellers.js?v=20260714pamfix1"></script>
|
<script defer src="assets/js/05-resellers.js?v=20260714pamfix1"></script>
|
||||||
<script defer src="assets/js/06-servers.js?v=20260714pamfix1"></script>
|
<script defer src="assets/js/06-servers.js?v=20260714pamfix1"></script>
|
||||||
<script defer src="assets/js/07-stats-logs.js?v=20260714pamfix1"></script>
|
<script defer src="assets/js/07-stats-logs.js?v=20260714pamfix1"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user