All checks were successful
Deploy / deploy (push) Successful in 22s
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.
110 lines
4.2 KiB
HTML
110 lines
4.2 KiB
HTML
{% if error %}
|
|
<p class="err">{{ error }}</p>
|
|
{% endif %}
|
|
{% if decisions %}
|
|
<p style="font-size:.8rem;color:#888;margin:.4rem 0;">
|
|
Showing <strong>{{ total }}</strong> decision{{ '' if total == 1 else 's' }}
|
|
(server limit {{ limit }}{% if total == limit %} — at cap, raise limit if more expected{% endif %}).
|
|
</p>
|
|
<form hx-post="/unban-bulk" hx-target="#decisions" hx-swap="innerHTML"
|
|
hx-confirm="Unban all selected decisions?" id="bulk-form">
|
|
<div class="row" style="margin: .5rem 0;">
|
|
<button class="danger" type="submit">Unban selected</button>
|
|
<span class="pill"><span id="sel-count">0</span> selected</span>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th><input type="checkbox" id="sel-all"></th>
|
|
<th>ID</th><th>IP / value</th><th>Scope</th><th>Type</th><th>Reason</th><th>Created</th><th>Until</th><th>Origin</th><th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for d in decisions %}
|
|
<tr>
|
|
<td><input type="checkbox" class="sel" name="id" value="{{ d.id }}"></td>
|
|
<td><code>{{ d.id }}</code></td>
|
|
<td><code>{{ d.value }}</code></td>
|
|
<td>{{ d.scope }}</td>
|
|
<td>{{ d.type }}</td>
|
|
<td>{{ d.scenario }}</td>
|
|
<td><time class="ts" datetime="{{ d.created_at }}">{{ d.created_at or '—' }}</time></td>
|
|
<td><time class="ts" datetime="{{ d.until }}">{{ d.until }}</time></td>
|
|
<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(){
|
|
const RTF = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
|
|
const FMT = new Intl.DateTimeFormat(undefined, {
|
|
year: 'numeric', month: 'short', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
hour12: false
|
|
});
|
|
const UNITS = [
|
|
['year', 31536000],
|
|
['month', 2592000],
|
|
['day', 86400],
|
|
['hour', 3600],
|
|
['minute', 60],
|
|
['second', 1],
|
|
];
|
|
function relative(target, now){
|
|
const diff = (target - now) / 1000;
|
|
const abs = Math.abs(diff);
|
|
for (const [u, s] of UNITS) {
|
|
if (abs >= s || u === 'second') return RTF.format(Math.round(diff / s), u);
|
|
}
|
|
}
|
|
function render(){
|
|
const now = new Date();
|
|
document.querySelectorAll('#decisions time.ts').forEach(t => {
|
|
const iso = t.getAttribute('datetime');
|
|
if (!iso) return;
|
|
const d = new Date(iso);
|
|
if (isNaN(d)) return;
|
|
t.textContent = FMT.format(d);
|
|
t.title = iso + ' (' + relative(d, now) + ')';
|
|
const small = document.createElement('small');
|
|
small.style.color = '#888';
|
|
small.style.marginLeft = '.35rem';
|
|
small.textContent = relative(d, now);
|
|
t.appendChild(small);
|
|
});
|
|
}
|
|
render();
|
|
setInterval(render, 30000);
|
|
})();
|
|
(function(){
|
|
const form = document.getElementById('bulk-form');
|
|
const all = document.getElementById('sel-all');
|
|
const cnt = document.getElementById('sel-count');
|
|
function refresh(){
|
|
const sel = form.querySelectorAll('.sel');
|
|
const ck = form.querySelectorAll('.sel:checked');
|
|
cnt.textContent = ck.length;
|
|
if (all) all.checked = (sel.length > 0 && ck.length === sel.length);
|
|
}
|
|
if (all) all.addEventListener('change', e => {
|
|
const checked = e.target.checked;
|
|
// Only toggle visible rows so a filtered subset can be bulk-selected
|
|
form.querySelectorAll('tbody tr:not(.hidden-row) .sel').forEach(c => c.checked = checked);
|
|
refresh();
|
|
});
|
|
form.addEventListener('change', e => { if (e.target.classList.contains('sel')) refresh(); });
|
|
refresh();
|
|
})();
|
|
</script>
|
|
{% else %}
|
|
{% if not error %}<p>No active decisions.</p>{% endif %}
|
|
{% endif %}
|