purgatory improvements (UI)
This commit is contained in:
@@ -40,12 +40,25 @@ const denyReasonInput = document.getElementById("deny-reason-input");
|
||||
const denyReasonModalConfirm = document.getElementById("deny-reason-modal-confirm");
|
||||
const denyReasonModalCancel = document.getElementById("deny-reason-modal-cancel");
|
||||
const denyReasonModalBackdrop = document.getElementById("deny-reason-modal-backdrop");
|
||||
const paginationEl = document.getElementById("enrollment-pagination");
|
||||
const pageSummaryEl = document.getElementById("enrollment-page-summary");
|
||||
const pageSizeEl = document.getElementById("enrollment-page-size");
|
||||
const prevPageBtn = document.getElementById("enrollment-prev-page");
|
||||
const nextPageBtn = document.getElementById("enrollment-next-page");
|
||||
const pageLabelEl = document.getElementById("enrollment-page-label");
|
||||
|
||||
let currentFilter = "pending";
|
||||
let searchQuery = "";
|
||||
let clients = [];
|
||||
const expandedCells = new Set(); // tracks "clientId:field" keys
|
||||
let lastEnrollmentDigest = "";
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = 100;
|
||||
try {
|
||||
rowsPerPage = Number(localStorage.getItem("purgatory_rows_per_page") || 100);
|
||||
} catch {}
|
||||
if (![50, 100, 200].includes(rowsPerPage)) rowsPerPage = 100;
|
||||
if (pageSizeEl) pageSizeEl.value = String(rowsPerPage);
|
||||
|
||||
const SUSPICIOUS_FLAG_LABELS = {
|
||||
hwid_flood: "HWID Flood (40+ same hardware ID)",
|
||||
@@ -105,6 +118,7 @@ async function loadClients() {
|
||||
const hasRows = body.querySelector("tr[data-id]");
|
||||
loadingRow.classList.toggle("hidden", !!hasRows);
|
||||
emptyEl.classList.add("hidden");
|
||||
if (!hasRows) paginationEl?.classList.add("hidden");
|
||||
|
||||
const fetchFilter = currentFilter;
|
||||
try {
|
||||
@@ -117,20 +131,8 @@ async function loadClients() {
|
||||
// Sort by newest first (highest lastSeen = most recent)
|
||||
clients.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0));
|
||||
|
||||
// Apply suspicious tab filter
|
||||
let base = clients;
|
||||
if (currentFilter === "suspicious") {
|
||||
base = clients.filter((c) => (c.suspiciousFlags || []).length > 0);
|
||||
}
|
||||
|
||||
// Apply search filter
|
||||
const filtered = getFilteredClients();
|
||||
const q = searchQuery.toLowerCase().trim();
|
||||
const filtered = q
|
||||
? base.filter((c) => {
|
||||
const fields = [c.host, c.user, c.ip, c.id, c.os, c.country, c.keyFingerprint, c.cpu, c.gpu, c.ram];
|
||||
return fields.some((f) => f && String(f).toLowerCase().includes(q));
|
||||
})
|
||||
: base;
|
||||
|
||||
loadingRow.classList.add("hidden");
|
||||
|
||||
@@ -143,11 +145,20 @@ async function loadClients() {
|
||||
lastEnrollmentDigest = `empty:${currentFilter}:${q}`;
|
||||
body.querySelectorAll("tr:not(#enrollment-loading)").forEach((r) => r.remove());
|
||||
emptyEl.classList.remove("hidden");
|
||||
paginationEl?.classList.add("hidden");
|
||||
paginationEl?.classList.remove("flex");
|
||||
updateBulkButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
const nextDigest = tableDigest(filtered, [
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / rowsPerPage));
|
||||
if (currentPage > totalPages) currentPage = totalPages;
|
||||
if (currentPage < 1) currentPage = 1;
|
||||
const startIndex = (currentPage - 1) * rowsPerPage;
|
||||
const visible = filtered.slice(startIndex, startIndex + rowsPerPage);
|
||||
updatePagination(filtered.length, startIndex, visible.length, totalPages);
|
||||
|
||||
const nextDigest = tableDigest(visible, [
|
||||
"id",
|
||||
"host",
|
||||
"user",
|
||||
@@ -162,7 +173,7 @@ async function loadClients() {
|
||||
"enrollmentStatus",
|
||||
"denyReason",
|
||||
"suspiciousFlags",
|
||||
]);
|
||||
], { total: filtered.length, page: currentPage, rowsPerPage, filter: currentFilter, q });
|
||||
if (nextDigest === lastEnrollmentDigest) {
|
||||
updateBulkButtons();
|
||||
return;
|
||||
@@ -172,7 +183,8 @@ async function loadClients() {
|
||||
const selectedBeforeRender = new Set(getSelectedIds());
|
||||
body.querySelectorAll("tr:not(#enrollment-loading)").forEach((r) => r.remove());
|
||||
|
||||
for (const c of filtered) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (const c of visible) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "group hover:bg-slate-800/45 transition-colors";
|
||||
tr.dataset.id = c.id;
|
||||
@@ -208,17 +220,45 @@ async function loadClients() {
|
||||
<td class="px-4 py-3"><div class="flex items-center justify-end gap-2 flex-nowrap">${actionButtons(c)}</div></td>
|
||||
`;
|
||||
tr.classList.add("cursor-pointer");
|
||||
body.appendChild(tr);
|
||||
fragment.appendChild(tr);
|
||||
}
|
||||
body.appendChild(fragment);
|
||||
updateBulkButtons();
|
||||
}
|
||||
|
||||
function tableDigest(items, fields) {
|
||||
return JSON.stringify(items.map((item) => {
|
||||
function getFilteredClients() {
|
||||
let base = clients;
|
||||
if (currentFilter === "suspicious") {
|
||||
base = clients.filter((c) => (c.suspiciousFlags || []).length > 0);
|
||||
}
|
||||
|
||||
const q = searchQuery.toLowerCase().trim();
|
||||
if (!q) return base;
|
||||
|
||||
return base.filter((c) => {
|
||||
const fields = [c.host, c.user, c.ip, c.id, c.os, c.country, c.keyFingerprint, c.cpu, c.gpu, c.ram];
|
||||
return fields.some((f) => f && String(f).toLowerCase().includes(q));
|
||||
});
|
||||
}
|
||||
|
||||
function tableDigest(items, fields, meta = {}) {
|
||||
return JSON.stringify({ meta, rows: items.map((item) => {
|
||||
const row = {};
|
||||
for (const field of fields) row[field] = item[field] ?? null;
|
||||
return row;
|
||||
}));
|
||||
}) });
|
||||
}
|
||||
|
||||
function updatePagination(total, startIndex, visibleCount, totalPages) {
|
||||
if (!paginationEl || !pageSummaryEl || !pageLabelEl || !prevPageBtn || !nextPageBtn) return;
|
||||
paginationEl.classList.toggle("hidden", total <= rowsPerPage);
|
||||
paginationEl.classList.toggle("flex", total > rowsPerPage);
|
||||
const from = total === 0 ? 0 : startIndex + 1;
|
||||
const to = startIndex + visibleCount;
|
||||
pageSummaryEl.textContent = `Showing ${from}-${to} of ${total}`;
|
||||
pageLabelEl.textContent = `Page ${currentPage} / ${totalPages}`;
|
||||
prevPageBtn.disabled = currentPage <= 1;
|
||||
nextPageBtn.disabled = currentPage >= totalPages;
|
||||
}
|
||||
|
||||
function statusBadge(status) {
|
||||
@@ -369,6 +409,8 @@ function updateBulkButtons() {
|
||||
document.querySelectorAll(".enrollment-tab").forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
currentFilter = tab.dataset.filter;
|
||||
currentPage = 1;
|
||||
lastEnrollmentDigest = "";
|
||||
document.querySelectorAll(".enrollment-tab").forEach((t) => {
|
||||
t.className =
|
||||
"enrollment-tab px-4 py-2 rounded-md text-sm font-medium bg-slate-800 text-slate-400 border border-slate-700 hover:bg-slate-700";
|
||||
@@ -467,10 +509,36 @@ searchInput.addEventListener("input", () => {
|
||||
clearTimeout(searchDebounce);
|
||||
searchDebounce = setTimeout(() => {
|
||||
searchQuery = searchInput.value;
|
||||
currentPage = 1;
|
||||
lastEnrollmentDigest = "";
|
||||
loadClients();
|
||||
}, 250);
|
||||
});
|
||||
|
||||
pageSizeEl?.addEventListener("change", () => {
|
||||
rowsPerPage = Number(pageSizeEl.value) || 100;
|
||||
if (![50, 100, 200].includes(rowsPerPage)) rowsPerPage = 100;
|
||||
try {
|
||||
localStorage.setItem("purgatory_rows_per_page", String(rowsPerPage));
|
||||
} catch {}
|
||||
currentPage = 1;
|
||||
lastEnrollmentDigest = "";
|
||||
loadClients();
|
||||
});
|
||||
|
||||
prevPageBtn?.addEventListener("click", () => {
|
||||
if (currentPage <= 1) return;
|
||||
currentPage -= 1;
|
||||
lastEnrollmentDigest = "";
|
||||
loadClients();
|
||||
});
|
||||
|
||||
nextPageBtn?.addEventListener("click", () => {
|
||||
currentPage += 1;
|
||||
lastEnrollmentDigest = "";
|
||||
loadClients();
|
||||
});
|
||||
|
||||
// ── Ban confirmation modal ──────────────────────────────────────────
|
||||
let banModalResolve = null;
|
||||
|
||||
@@ -661,10 +729,9 @@ unlessSuspiciousToggle.addEventListener("change", async () => {
|
||||
|
||||
// ── Approve All / Deny All ────────────────────────────────────────
|
||||
function getActionablePendingIds() {
|
||||
const base = currentFilter === "suspicious"
|
||||
? clients.filter((c) => (c.suspiciousFlags || []).length > 0)
|
||||
: clients;
|
||||
return base.filter((c) => (c.enrollmentStatus || "pending") === "pending").map((c) => c.id);
|
||||
return getFilteredClients()
|
||||
.filter((c) => (c.enrollmentStatus || "pending") === "pending")
|
||||
.map((c) => c.id);
|
||||
}
|
||||
|
||||
approveAllBtn.addEventListener("click", async () => {
|
||||
|
||||
@@ -181,6 +181,29 @@
|
||||
<i class="fa-solid fa-inbox text-3xl mb-3 block"></i>
|
||||
<p>No clients in this category.</p>
|
||||
</div>
|
||||
<div id="enrollment-pagination" class="hidden items-center justify-between gap-3 border-t border-slate-800 bg-slate-950/35 px-4 py-3 text-sm text-slate-400">
|
||||
<div id="enrollment-page-summary">Showing 0-0 of 0</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="flex items-center gap-2">
|
||||
<span>Rows</span>
|
||||
<select id="enrollment-page-size"
|
||||
class="rounded-md border border-slate-700 bg-slate-900 px-2 py-1 text-slate-200 focus:outline-none focus:ring-2 focus:ring-sky-500">
|
||||
<option value="50">50</option>
|
||||
<option value="100" selected>100</option>
|
||||
<option value="200">200</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="enrollment-prev-page" type="button"
|
||||
class="rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-slate-200 hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-45">
|
||||
<i class="fa-solid fa-chevron-left"></i>
|
||||
</button>
|
||||
<span id="enrollment-page-label" class="min-w-[76px] text-center text-slate-300">Page 1 / 1</span>
|
||||
<button id="enrollment-next-page" type="button"
|
||||
class="rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-slate-200 hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-45">
|
||||
<i class="fa-solid fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Banned IPs Table (hidden by default, shown on Banned IPs tab) -->
|
||||
|
||||
Reference in New Issue
Block a user