threat-intel: master 'select all' checkbox in toolbar

UX request 2026-04-30: when sorting by threat in the threat intel
panel, ban-selected required clicking each per-row checkbox
individually. Pages with 20-50 threats made bulk-ban tedious.

Adds a master `[ ] all` checkbox to the toolbar (right of the
Sort buttons, left of the existing 'N selected' counter) that
toggles every per-row .ip-check on the page in one click. Then
'Ban Selected' / 'Unban Selected' work over the whole set.

Three-state: unchecked (none selected) / checked (all) /
indeterminate (partial — browsers render this as a "half-tick"
so operators get visual feedback when they've toggled some rows
manually after using master). updateSelCount keeps the master
in sync as individual rows toggle so the visual is always
truthful.

No backend change — `/api/admin/security/mass-ban` already
accepts an arbitrary IP list. This is purely a frontend
ergonomics improvement on top of the existing mass-action
infrastructure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-04-30 03:27:20 -05:00
parent b09b73c409
commit 2575842f7b

View File

@ -1462,6 +1462,22 @@ async function loadThreats() {
});
// Mass action buttons
var spacer = document.createElement('div'); spacer.style.flex = '1'; toolbar.appendChild(spacer);
// Master "select all on this page" checkbox (2026-04-30 J UX request).
// Mirrors the per-row .ip-check style; toggles every visible row.
// Three-state: unchecked (none selected), checked (all selected),
// indeterminate (partial). updateSelCount keeps it in sync as
// individual rows are toggled.
var selAllWrap = document.createElement('label');
selAllWrap.style.cssText = 'display:flex;align-items:center;gap:6px;font-family:JetBrains Mono,monospace;font-size:9px;text-transform:uppercase;letter-spacing:0.5px;color:#7a7872;cursor:pointer';
selAllWrap.title = 'Toggle every IP on this page';
var selAll = document.createElement('input'); selAll.type = 'checkbox';
selAll.id = 'sel-all';
selAll.style.cssText = 'width:16px;height:16px;cursor:pointer;accent-color:#e2b55a';
selAll.onchange = function(){ toggleAllChecks(this.checked); };
selAllWrap.appendChild(selAll);
var selAllLabel = document.createElement('span'); selAllLabel.textContent = 'all';
selAllWrap.appendChild(selAllLabel);
toolbar.appendChild(selAllWrap);
var selCount = document.createElement('span'); selCount.id = 'sel-count';
selCount.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:10px;color:#7a7872';
toolbar.appendChild(selCount);
@ -1585,9 +1601,33 @@ async function loadThreats() {
var currentSort = 'hits';
function updateSelCount() {
var checks = document.querySelectorAll('.ip-check:checked');
var all = document.querySelectorAll('.ip-check');
var checked = document.querySelectorAll('.ip-check:checked');
var el = document.getElementById('sel-count');
if (el) el.textContent = checks.length ? checks.length + ' selected' : '';
if (el) el.textContent = checked.length ? checked.length + ' selected' : '';
// Sync the master "all" checkbox to reflect the page's actual state.
// Three states: none unchecked, all checked, partial indeterminate.
// Indeterminate is the visual "half-tick" most browsers render gives
// operators a clear "you've got some but not all selected" hint.
var master = document.getElementById('sel-all');
if (master) {
if (checked.length === 0) {
master.checked = false; master.indeterminate = false;
} else if (checked.length === all.length) {
master.checked = true; master.indeterminate = false;
} else {
master.indeterminate = true;
}
}
}
function toggleAllChecks(checked) {
// Master "select all" handler flips every per-row checkbox on the
// page to match the master's state. Used by the toolbar's `[ ] all`
// checkbox so operators don't have to click each threat individually
// before hitting Ban Selected. (2026-04-30 J UX request.)
document.querySelectorAll('.ip-check').forEach(function(cb){ cb.checked = checked; });
updateSelCount();
}
async function massAction(action) {