From fa990c2094f94231b0849e99bd02addb9466ac91 Mon Sep 17 00:00:00 2001 From: penguinehis Date: Tue, 14 Jul 2026 00:48:57 -0300 Subject: [PATCH] fix user list --- admin/assets/app.css | 7 ++++ admin/assets/js/03-ssh-users.js | 70 +++++++++++++++++++++++++++++++++ admin/index.html | 6 +-- 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/admin/assets/app.css b/admin/assets/app.css index 8f1eefd..941692d 100644 --- a/admin/assets/app.css +++ b/admin/assets/app.css @@ -750,3 +750,10 @@ select:disabled { @media(max-width:1180px){.bot-overview-grid{grid-template-columns:repeat(2,minmax(0,1fr));}.bot-section-nav{grid-template-columns:repeat(3,minmax(0,1fr));}.bot-master-detail{grid-template-columns:1fr;}.bot-editor-card{position:static;}.bot-nav-shell{top:78px;}} @media(max-width:760px){.bot-hero{padding:20px;border-radius:22px;}.bot-hero h2{font-size:1.55rem;}.bot-hero-actions{position:relative;right:auto;top:auto;max-width:none;justify-content:flex-start;margin-top:16px;}.bot-overview-grid{grid-template-columns:1fr 1fr;margin-top:18px;}.bot-section-nav{display:none;}.bot-section-select{display:block;}.bot-nav-shell{top:76px;}.bot-config-grid,.bot-message-grid{grid-template-columns:1fr;}.bot-section-heading{align-items:flex-start;flex-direction:column;}.bot-section-heading>.card-actions{width:100%;justify-content:flex-start;}.bot-master-detail{display:block;}.bot-master-detail>.card+.card{margin-top:14px!important;}.bot-save-row{align-items:flex-start;flex-direction:column;}} @media(max-width:460px){.bot-overview-grid{grid-template-columns:1fr;}.bot-overview-card{padding:11px 12px;}.bot-hero-actions .btn{width:100%;}.bot-section-heading .btn{width:100%;}.bot-copy-row{align-items:stretch;flex-direction:column;}.bot-copy-row .btn{width:100%;}} + +/* Sortable SSH user table headers */ +th[data-sort-key]{cursor:pointer;user-select:none;white-space:nowrap;transition:color .12s ease;} +th[data-sort-key]:hover{color:var(--accent);} +th[data-sort-key]::after{content:"";display:inline-block;width:.9em;font-size:.72em;opacity:.85;} +th[data-sort-key].sort-asc::after{content:" \25B2";} +th[data-sort-key].sort-desc::after{content:" \25BC";} diff --git a/admin/assets/js/03-ssh-users.js b/admin/assets/js/03-ssh-users.js index 63f9182..24fdc50 100644 --- a/admin/assets/js/03-ssh-users.js +++ b/admin/assets/js/03-ssh-users.js @@ -57,7 +57,77 @@ async function loadUsersSilent() { } } +// ---- Column sorting (click a header to sort) ---- +// Value extractor per sortable column. Numbers sort numerically, strings +// alphabetically; online counts as 1 so "status" groups online users together. +const USER_SORT_EXTRACT = { + username: u => String(u.username || "").toLowerCase(), + status: u => (u.active_conns || 0) > 0 ? 1 : 0, + auth: u => u.use_pam ? "pam" : (u.totp_enabled ? (u.allow_static_password ? "totp+pw" : "totp") : "password"), + conn: u => u.active_conns || 0, + max: u => u.max_connections || 0, + up: u => u.limit_mbps_up || 0, + down: u => u.limit_mbps_down || 0, + expires: u => u.expires_at ? new Date(u.expires_at).getTime() : Infinity, + owner: u => String(u.owner_username || "").toLowerCase(), +}; +// Columns that default to descending on first click (most/online first). +const USER_SORT_DEFAULT_DESC = new Set(["status", "conn", "max", "up", "down"]); + +let userSort = { key: "username", dir: "asc" }; +let lastUsersData = []; + +function sortUsers(list) { + const ext = USER_SORT_EXTRACT[userSort.key] || USER_SORT_EXTRACT.username; + const dir = userSort.dir === "desc" ? -1 : 1; + return list.slice().sort((a, b) => { + const va = ext(a), vb = ext(b); + let cmp; + if (typeof va === "number" && typeof vb === "number") cmp = va - vb; + else cmp = String(va).localeCompare(String(vb)); + // Stable tie-break by username so equal rows never shuffle between polls. + if (cmp === 0) cmp = String(a.username || "").localeCompare(String(b.username || "")); + return cmp * dir; + }); +} + +function updateSortIndicators() { + const table = usersBody && usersBody.closest("table"); + if (!table) return; + table.querySelectorAll("th[data-sort-key]").forEach(th => { + th.classList.remove("sort-asc", "sort-desc"); + if (th.getAttribute("data-sort-key") === userSort.key) { + th.classList.add(userSort.dir === "asc" ? "sort-asc" : "sort-desc"); + } + }); +} + +function setUserSort(key) { + if (!USER_SORT_EXTRACT[key]) return; + if (userSort.key === key) { + userSort.dir = userSort.dir === "asc" ? "desc" : "asc"; + } else { + userSort.key = key; + userSort.dir = USER_SORT_DEFAULT_DESC.has(key) ? "desc" : "asc"; + } + updateSortIndicators(); + renderUsers(lastUsersData); +} + +(function initUserSortHeaders() { + const table = usersBody && usersBody.closest("table"); + if (!table) return; + table.querySelectorAll("th[data-sort-key]").forEach(th => { + th.addEventListener("click", () => setUserSort(th.getAttribute("data-sort-key"))); + }); + updateSortIndicators(); +})(); + function renderUsers(users) { + // 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. + lastUsersData = users || []; + users = sortUsers(lastUsersData); updateDashboardFromUsers(users); const isSA = currentRole === "superadmin"; userCountChip.textContent = users.length; diff --git a/admin/index.html b/admin/index.html index 90e0f54..2908cbc 100644 --- a/admin/index.html +++ b/admin/index.html @@ -272,9 +272,9 @@
- - - + + +
UserStatusAuthConnMaxUpDnExpiresUserStatusAuthConnMaxUpDnExpires Actions