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 denyReasonModalConfirm = document.getElementById("deny-reason-modal-confirm");
|
||||||
const denyReasonModalCancel = document.getElementById("deny-reason-modal-cancel");
|
const denyReasonModalCancel = document.getElementById("deny-reason-modal-cancel");
|
||||||
const denyReasonModalBackdrop = document.getElementById("deny-reason-modal-backdrop");
|
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 currentFilter = "pending";
|
||||||
let searchQuery = "";
|
let searchQuery = "";
|
||||||
let clients = [];
|
let clients = [];
|
||||||
const expandedCells = new Set(); // tracks "clientId:field" keys
|
const expandedCells = new Set(); // tracks "clientId:field" keys
|
||||||
let lastEnrollmentDigest = "";
|
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 = {
|
const SUSPICIOUS_FLAG_LABELS = {
|
||||||
hwid_flood: "HWID Flood (40+ same hardware ID)",
|
hwid_flood: "HWID Flood (40+ same hardware ID)",
|
||||||
@@ -105,6 +118,7 @@ async function loadClients() {
|
|||||||
const hasRows = body.querySelector("tr[data-id]");
|
const hasRows = body.querySelector("tr[data-id]");
|
||||||
loadingRow.classList.toggle("hidden", !!hasRows);
|
loadingRow.classList.toggle("hidden", !!hasRows);
|
||||||
emptyEl.classList.add("hidden");
|
emptyEl.classList.add("hidden");
|
||||||
|
if (!hasRows) paginationEl?.classList.add("hidden");
|
||||||
|
|
||||||
const fetchFilter = currentFilter;
|
const fetchFilter = currentFilter;
|
||||||
try {
|
try {
|
||||||
@@ -117,20 +131,8 @@ async function loadClients() {
|
|||||||
// Sort by newest first (highest lastSeen = most recent)
|
// Sort by newest first (highest lastSeen = most recent)
|
||||||
clients.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0));
|
clients.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0));
|
||||||
|
|
||||||
// Apply suspicious tab filter
|
const filtered = getFilteredClients();
|
||||||
let base = clients;
|
|
||||||
if (currentFilter === "suspicious") {
|
|
||||||
base = clients.filter((c) => (c.suspiciousFlags || []).length > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply search filter
|
|
||||||
const q = searchQuery.toLowerCase().trim();
|
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");
|
loadingRow.classList.add("hidden");
|
||||||
|
|
||||||
@@ -143,11 +145,20 @@ async function loadClients() {
|
|||||||
lastEnrollmentDigest = `empty:${currentFilter}:${q}`;
|
lastEnrollmentDigest = `empty:${currentFilter}:${q}`;
|
||||||
body.querySelectorAll("tr:not(#enrollment-loading)").forEach((r) => r.remove());
|
body.querySelectorAll("tr:not(#enrollment-loading)").forEach((r) => r.remove());
|
||||||
emptyEl.classList.remove("hidden");
|
emptyEl.classList.remove("hidden");
|
||||||
|
paginationEl?.classList.add("hidden");
|
||||||
|
paginationEl?.classList.remove("flex");
|
||||||
updateBulkButtons();
|
updateBulkButtons();
|
||||||
return;
|
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",
|
"id",
|
||||||
"host",
|
"host",
|
||||||
"user",
|
"user",
|
||||||
@@ -162,7 +173,7 @@ async function loadClients() {
|
|||||||
"enrollmentStatus",
|
"enrollmentStatus",
|
||||||
"denyReason",
|
"denyReason",
|
||||||
"suspiciousFlags",
|
"suspiciousFlags",
|
||||||
]);
|
], { total: filtered.length, page: currentPage, rowsPerPage, filter: currentFilter, q });
|
||||||
if (nextDigest === lastEnrollmentDigest) {
|
if (nextDigest === lastEnrollmentDigest) {
|
||||||
updateBulkButtons();
|
updateBulkButtons();
|
||||||
return;
|
return;
|
||||||
@@ -172,7 +183,8 @@ async function loadClients() {
|
|||||||
const selectedBeforeRender = new Set(getSelectedIds());
|
const selectedBeforeRender = new Set(getSelectedIds());
|
||||||
body.querySelectorAll("tr:not(#enrollment-loading)").forEach((r) => r.remove());
|
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");
|
const tr = document.createElement("tr");
|
||||||
tr.className = "group hover:bg-slate-800/45 transition-colors";
|
tr.className = "group hover:bg-slate-800/45 transition-colors";
|
||||||
tr.dataset.id = c.id;
|
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>
|
<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");
|
tr.classList.add("cursor-pointer");
|
||||||
body.appendChild(tr);
|
fragment.appendChild(tr);
|
||||||
}
|
}
|
||||||
|
body.appendChild(fragment);
|
||||||
updateBulkButtons();
|
updateBulkButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function tableDigest(items, fields) {
|
function getFilteredClients() {
|
||||||
return JSON.stringify(items.map((item) => {
|
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 = {};
|
const row = {};
|
||||||
for (const field of fields) row[field] = item[field] ?? null;
|
for (const field of fields) row[field] = item[field] ?? null;
|
||||||
return row;
|
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) {
|
function statusBadge(status) {
|
||||||
@@ -369,6 +409,8 @@ function updateBulkButtons() {
|
|||||||
document.querySelectorAll(".enrollment-tab").forEach((tab) => {
|
document.querySelectorAll(".enrollment-tab").forEach((tab) => {
|
||||||
tab.addEventListener("click", () => {
|
tab.addEventListener("click", () => {
|
||||||
currentFilter = tab.dataset.filter;
|
currentFilter = tab.dataset.filter;
|
||||||
|
currentPage = 1;
|
||||||
|
lastEnrollmentDigest = "";
|
||||||
document.querySelectorAll(".enrollment-tab").forEach((t) => {
|
document.querySelectorAll(".enrollment-tab").forEach((t) => {
|
||||||
t.className =
|
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";
|
"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);
|
clearTimeout(searchDebounce);
|
||||||
searchDebounce = setTimeout(() => {
|
searchDebounce = setTimeout(() => {
|
||||||
searchQuery = searchInput.value;
|
searchQuery = searchInput.value;
|
||||||
|
currentPage = 1;
|
||||||
|
lastEnrollmentDigest = "";
|
||||||
loadClients();
|
loadClients();
|
||||||
}, 250);
|
}, 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 ──────────────────────────────────────────
|
// ── Ban confirmation modal ──────────────────────────────────────────
|
||||||
let banModalResolve = null;
|
let banModalResolve = null;
|
||||||
|
|
||||||
@@ -661,10 +729,9 @@ unlessSuspiciousToggle.addEventListener("change", async () => {
|
|||||||
|
|
||||||
// ── Approve All / Deny All ────────────────────────────────────────
|
// ── Approve All / Deny All ────────────────────────────────────────
|
||||||
function getActionablePendingIds() {
|
function getActionablePendingIds() {
|
||||||
const base = currentFilter === "suspicious"
|
return getFilteredClients()
|
||||||
? clients.filter((c) => (c.suspiciousFlags || []).length > 0)
|
.filter((c) => (c.enrollmentStatus || "pending") === "pending")
|
||||||
: clients;
|
.map((c) => c.id);
|
||||||
return base.filter((c) => (c.enrollmentStatus || "pending") === "pending").map((c) => c.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
approveAllBtn.addEventListener("click", async () => {
|
approveAllBtn.addEventListener("click", async () => {
|
||||||
|
|||||||
@@ -181,6 +181,29 @@
|
|||||||
<i class="fa-solid fa-inbox text-3xl mb-3 block"></i>
|
<i class="fa-solid fa-inbox text-3xl mb-3 block"></i>
|
||||||
<p>No clients in this category.</p>
|
<p>No clients in this category.</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Banned IPs Table (hidden by default, shown on Banned IPs tab) -->
|
<!-- Banned IPs Table (hidden by default, shown on Banned IPs tab) -->
|
||||||
|
|||||||
Reference in New Issue
Block a user