feat: bulk unban via checkboxes
All checks were successful
Deploy / deploy (push) Successful in 23s

Adds per-row checkboxes, a select-all toggle, a live "N selected" counter,
and an /unban-bulk endpoint that DELETEs each chosen decision id. Single-row
Unban buttons still work via hx-vals so they don't accidentally submit the
bulk form.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 00:02:59 +02:00
parent ffe08a3fc4
commit a324d5a68d
2 changed files with 64 additions and 24 deletions

View File

@@ -122,6 +122,25 @@ def unban():
return list_decisions() return list_decisions()
@app.post("/unban-bulk")
def unban_bulk():
ids = [i for i in request.form.getlist("id") if i.isdigit()]
if not ids:
return list_decisions()
deleted = 0
errors = []
for did in ids:
r = _machine("DELETE", f"/v1/decisions/{did}")
if r.status_code in (200, 204):
deleted += 1
else:
errors.append(f"{did}:{r.status_code}")
log.info("unban-bulk by=%s count=%d errors=%s", caller_ip(), deleted, errors)
if errors:
return f"deleted {deleted}/{len(ids)}; errors: {', '.join(errors[:5])}", 502
return list_decisions()
@app.post("/unban-me") @app.post("/unban-me")
def unban_me(): def unban_me():
ip = caller_ip() ip = caller_ip()

View File

@@ -2,30 +2,51 @@
<p class="err">{{ error }}</p> <p class="err">{{ error }}</p>
{% endif %} {% endif %}
{% if decisions %} {% if decisions %}
<table> <form hx-post="/unban-bulk" hx-target="#decisions" hx-swap="innerHTML"
<thead> hx-confirm="Unban all selected decisions?" id="bulk-form">
<tr><th>ID</th><th>IP / value</th><th>Scope</th><th>Type</th><th>Reason</th><th>Until</th><th>Origin</th><th></th></tr> <div class="row" style="margin: .5rem 0;">
</thead> <button class="danger" type="submit">Unban selected</button>
<tbody> <span class="pill"><span id="sel-count">0</span> selected of {{ decisions|length }}</span>
{% for d in decisions %} </div>
<tr> <table>
<td><code>{{ d.id }}</code></td> <thead>
<td><code>{{ d.value }}</code></td> <tr>
<td>{{ d.scope }}</td> <th><input type="checkbox" id="sel-all" onclick="document.querySelectorAll('#bulk-form .sel').forEach(c=>{c.checked=this.checked});updateCount()"></th>
<td>{{ d.type }}</td> <th>ID</th><th>IP / value</th><th>Scope</th><th>Type</th><th>Reason</th><th>Until</th><th>Origin</th><th></th>
<td>{{ d.scenario }}</td> </tr>
<td>{{ d.until }}</td> </thead>
<td>{{ d.origin }}</td> <tbody>
<td> {% for d in decisions %}
<form hx-post="/unban" hx-target="#decisions" hx-swap="innerHTML" hx-confirm="Delete decision {{ d.id }} for {{ d.value }}?"> <tr>
<input type="hidden" name="id" value="{{ d.id }}"> <td><input type="checkbox" class="sel" name="id" value="{{ d.id }}" onclick="updateCount()"></td>
<button class="danger" type="submit">Unban</button> <td><code>{{ d.id }}</code></td>
</form> <td><code>{{ d.value }}</code></td>
</td> <td>{{ d.scope }}</td>
</tr> <td>{{ d.type }}</td>
{% endfor %} <td>{{ d.scenario }}</td>
</tbody> <td>{{ d.until }}</td>
</table> <td>{{ d.origin }}</td>
<td>
<button class="danger" type="button"
hx-post="/unban" hx-target="#decisions" hx-swap="innerHTML"
hx-vals='{"id": "{{ d.id }}"}'
hx-confirm="Delete decision {{ d.id }} for {{ d.value }}?">Unban</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
<script>
function updateCount(){
const n = document.querySelectorAll('#bulk-form .sel:checked').length;
const total = document.querySelectorAll('#bulk-form .sel').length;
document.getElementById('sel-count').textContent = n;
const all = document.getElementById('sel-all');
if (all) all.checked = (n === total && total > 0);
}
updateCount();
</script>
{% else %} {% else %}
{% if not error %}<p>No active decisions.</p>{% endif %} {% if not error %}<p>No active decisions.</p>{% endif %}
{% endif %} {% endif %}