Files
crowdsec-admin/app/templates/index.html
domverse 708f6373f1
All checks were successful
Deploy / deploy (push) Successful in 22s
feat: alerts view + created-at timestamps on decisions
Mirror cscli alerts list: new /alerts endpoint hits LAPI machine auth
with since/ip/scenario/origin filters, renders ID, scope:value, reason,
country, AS, events, decisions, created_at.

Decisions table gains a Created column derived from until - duration
(LAPI does not expose created_at on /v1/decisions). Both views format
timestamps locally and append a relative "x ago" via Intl.RelativeTimeFormat,
refreshed every 30s on the decisions view.
2026-06-21 15:13:14 +02:00

126 lines
5.3 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>CrowdSec Admin</title>
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
<style>
:root { color-scheme: dark; }
body { font-family: system-ui, sans-serif; max-width: 1100px; margin: 2rem auto; padding: 0 1rem; background:#111; color:#ddd; }
h1 { margin-top: 0; }
table { border-collapse: collapse; width: 100%; margin-top: 1rem; }
th, td { padding: .4rem .6rem; border-bottom: 1px solid #333; text-align: left; font-size: 0.9rem; }
th { background: #222; }
tr:hover { background: #1a1a1a; }
button, input[type=submit] { background:#3a6; color:#fff; border:0; padding:.4rem .7rem; border-radius:4px; cursor:pointer; }
button.danger { background:#c44; }
button.warn { background:#a82; }
input[type=text] { background:#222; color:#ddd; border:1px solid #444; padding:.4rem .6rem; border-radius:4px; }
.row { display:flex; gap:.6rem; align-items:center; flex-wrap:wrap; }
.pill { background:#333; padding:.15rem .5rem; border-radius:99px; font-size:.8rem; }
.me { background:#2a4; }
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>
<h1>CrowdSec Admin</h1>
<div class="row">
<span class="pill me">Your IP: <code>{{ my_ip }}</code></span>
<form hx-post="/unban-me" hx-target="#unban-me-result" hx-swap="innerHTML" hx-confirm="Unban {{ my_ip }}?">
<button class="warn" type="submit">Unban my IP</button>
</form>
</div>
<div id="unban-me-result"></div>
<section>
<h2>Recent alerts</h2>
<form hx-get="/alerts" hx-target="#alerts" hx-trigger="submit, load delay:200ms" hx-swap="innerHTML" hx-indicator="#alerts" class="row">
<select name="since">
<option value="15m">15m</option>
<option value="1h" selected>1h</option>
<option value="6h">6h</option>
<option value="24h">24h</option>
<option value="168h">7d</option>
<option value="720h">30d</option>
</select>
<input type="text" name="ip" placeholder="IP (optional)">
<input type="text" name="scenario" placeholder="Scenario (optional)">
<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>
</select>
<button type="submit">Search</button>
</form>
<div id="alerts">Loading…</div>
</section>
<section>
<h2>Active decisions</h2>
<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>
</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>
<form hx-post="/unban" hx-target="#decisions" hx-swap="innerHTML" hx-confirm="Unban entered IP?" class="row">
<input type="text" name="ip" placeholder="e.g. 1.2.3.4" required>
<button class="danger" type="submit">Unban IP</button>
</form>
</section>
</body>
</html>