feat: server-side reason/origin/limit filters + client-side row filter
All checks were successful
Deploy / deploy (push) Successful in 17s
All checks were successful
Deploy / deploy (push) Successful in 17s
Two new performance + UX wins: - Server: /decisions now accepts reason (scenarios_containing), origin (origins), and limit. Default 200, max 2000. Header shows current count and warns when at the cap. - Client: a "quick filter rows" input does substring match across all visible columns instantly with no server round-trip. Useful when the server-side result is small enough to scroll but you still want to find one scenario fast. - Bulk select-all now toggles only currently visible rows so you can filter then bulk-unban. Drops inline onclick handlers in favor of delegated change listeners to keep per-row cost low at large limits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,11 @@
|
||||
section { margin-top: 1.5rem; padding: 1rem; background:#181818; border-radius:6px; }
|
||||
.err { color:#f88; }
|
||||
code { background:#222; padding:.1rem .3rem; border-radius:3px; }
|
||||
.row > * { flex: 0 0 auto; }
|
||||
.grow { flex: 1 1 200px; }
|
||||
.htmx-request #decisions::after { content: " loading…"; color:#888; }
|
||||
.hidden-row { display:none; }
|
||||
select { background:#222; color:#ddd; border:1px solid #444; padding:.4rem; border-radius:4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -36,13 +41,47 @@
|
||||
|
||||
<section>
|
||||
<h2>Active decisions</h2>
|
||||
<form hx-get="/decisions" hx-target="#decisions" hx-trigger="submit, load delay:200ms" hx-swap="innerHTML" class="row">
|
||||
<input type="text" name="ip" placeholder="Filter by IP (optional)">
|
||||
<form hx-get="/decisions" hx-target="#decisions" hx-trigger="submit, load delay:200ms" hx-swap="innerHTML" hx-indicator="#decisions" class="row">
|
||||
<input type="text" name="ip" placeholder="IP (server-side)">
|
||||
<input type="text" name="reason" placeholder="Reason contains (server-side)">
|
||||
<select name="origin">
|
||||
<option value="">any origin</option>
|
||||
<option value="crowdsec">crowdsec</option>
|
||||
<option value="cscli">cscli</option>
|
||||
<option value="CAPI">CAPI</option>
|
||||
<option value="lists">lists</option>
|
||||
</select>
|
||||
<select name="limit">
|
||||
<option value="100">100</option>
|
||||
<option value="200" selected>200</option>
|
||||
<option value="500">500</option>
|
||||
<option value="1000">1000</option>
|
||||
<option value="2000">2000</option>
|
||||
</select>
|
||||
<button type="submit">Search</button>
|
||||
<button type="button" hx-get="/decisions" hx-target="#decisions" hx-swap="innerHTML">Refresh all</button>
|
||||
</form>
|
||||
<div class="row" style="margin-top:.6rem;">
|
||||
<input type="text" id="client-filter" placeholder="Quick filter rows (any column)…" class="grow"
|
||||
oninput="filterRows()">
|
||||
<span class="pill" id="visible-count"></span>
|
||||
</div>
|
||||
<div id="decisions">Loading…</div>
|
||||
</section>
|
||||
<script>
|
||||
function filterRows(){
|
||||
const q = (document.getElementById('client-filter').value || '').toLowerCase();
|
||||
const rows = document.querySelectorAll('#decisions tbody tr');
|
||||
let shown = 0;
|
||||
rows.forEach(r => {
|
||||
const match = !q || r.textContent.toLowerCase().includes(q);
|
||||
r.classList.toggle('hidden-row', !match);
|
||||
if (match) shown++;
|
||||
});
|
||||
const p = document.getElementById('visible-count');
|
||||
if (p) p.textContent = shown + ' / ' + rows.length + ' visible';
|
||||
}
|
||||
document.body.addEventListener('htmx:afterSwap', e => { if (e.detail.target.id === 'decisions') filterRows(); });
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<h2>Unban by IP</h2>
|
||||
|
||||
Reference in New Issue
Block a user