feat(ui): add banned IPs management to debug panel

- Add debug section for banned IPs with refresh button
- Implement fetchBannedIPs() function to display banned IPs in table
- Add adminUnban() function for unbanning IPs from UI
- Show IP, ban time, reason, banned by, and unban action in table
This commit is contained in:
2026-05-13 13:36:37 +02:00
parent 75f9686533
commit ac759a60fe
+118 -1
View File
@@ -535,6 +535,11 @@
color: var(--danger);
}
.device-actions {
display: flex;
gap: 4px;
}
.confirm-text {
font-size: 15px;
margin-bottom: 24px;
@@ -675,12 +680,31 @@
<th>Requests</th>
<th>Last Seen</th>
<th>User Agent</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="debug-devices-tbody"></tbody>
</table>
</div>
</div>
<div class="debug-section">
<h3>🚫 Banned IPs</h3>
<div style="overflow-x: auto">
<table class="debug-table">
<thead>
<tr>
<th>IP</th>
<th>Banned At</th>
<th>Reason</th>
<th>Banned By</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="debug-banned-tbody"><tr><td colspan="5" style="color:var(--muted);text-align:center;">No banned IPs</td></tr></tbody>
</table>
</div>
</div>
</div>
</div>
@@ -903,7 +927,7 @@
<div class="field">
<label>Cache TTL (seconds)</label>
<input type="number" id="view-cache-ttl" value="${cacheTtl}" min="0" max="86400" required>
<div class="hint">How long a render stays in cache. 0 = always re-render. Each access extends TTL.</div>
<div class="hint">How long a render stays in cache. 0 = always re-render.</div>
</div>
<div class="field" style="display:flex;gap:12px;">
@@ -1164,6 +1188,7 @@
document.getElementById("debug-devices-tbody").innerHTML = data.devices
.map((d) => {
const ago = Math.round((Date.now() - d.lastSeen) / 1000);
const ipEnc = encodeURIComponent(d.ip);
return `
<tr>
<td>${esc(d.ip)}</td>
@@ -1171,10 +1196,20 @@
<td>${d.requestCount}</td>
<td>${ago}s ago</td>
<td class="url-cell" title="${esc(d.userAgent)}">${esc(d.userAgent.slice(0, 60))}</td>
<td>
<div class="device-actions">
<button class="btn btn-xs btn-ghost" onclick="adminRefresh('${ipEnc}')" title="Force cache refresh">↺</button>
<button class="btn btn-xs btn-ghost" onclick="adminDisconnect('${ipEnc}')" title="Disconnect">⏏</button>
<button class="btn btn-xs btn-danger" onclick="adminBan('${ipEnc}', '${esc(d.ip)}')" title="Ban IP">🚫</button>
</div>
</td>
</tr>`;
})
.join("");
// ── Banned IPs table ──
fetchBannedIPs();
if (!debugTickInterval) {
debugTickInterval = setInterval(tickDebugValues, 500);
}
@@ -1203,6 +1238,88 @@
});
}
// ═══════════════════════════════════════════════
// ADMIN ACTIONS
// ═══════════════════════════════════════════════
async function adminDisconnect(ipEnc) {
try {
await api("POST", "/api/admin/disconnect/" + ipEnc);
toast("Device disconnected");
} catch (err) {
toast(err.message, "error");
}
}
async function adminRefresh(ipEnc) {
try {
await api("POST", "/api/admin/refresh/" + ipEnc);
toast("Cache refreshed");
} catch (err) {
toast(err.message, "error");
}
}
function adminBan(ipEnc, ipDisplay) {
openModal(`
<h2>Ban IP</h2>
<div class="confirm-text">Ban <strong>${esc(ipDisplay)}</strong>? Future connections will receive 403.</div>
<div class="field">
<label>Reason</label>
<input type="text" id="ban-reason" placeholder="Reason (optional)" value="Banned by administrator">
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
<button class="btn btn-danger" onclick="doAdminBan('${ipEnc}', '${esc(ipDisplay)}')">Ban</button>
</div>`);
}
async function doAdminBan(ipEnc, ipDisplay) {
const reason = document.getElementById("ban-reason")?.value || "Banned by administrator";
try {
await api("POST", "/api/admin/ban/" + ipEnc, { reason });
closeModal();
toast(`Banned ${decodeURIComponent(ipEnc)}`);
fetchBannedIPs();
} catch (err) {
toast(err.message, "error");
}
}
async function adminUnban(ipEnc) {
try {
await api("POST", "/api/admin/unban/" + ipEnc);
toast("IP unbanned");
fetchBannedIPs();
} catch (err) {
toast(err.message, "error");
}
}
async function fetchBannedIPs() {
try {
const banned = await api("GET", "/api/banned");
const tbody = document.getElementById("debug-banned-tbody");
if (!tbody) return;
if (!banned.length) {
tbody.innerHTML = '<tr><td colspan="5" style="color:var(--muted);text-align:center;">No banned IPs</td></tr>';
return;
}
tbody.innerHTML = banned.map((b) => {
const ipEnc = encodeURIComponent(b.ip);
const dt = new Date(b.banned_at * 1000).toLocaleString();
return `
<tr>
<td>${esc(b.ip)}</td>
<td>${esc(dt)}</td>
<td>${esc(b.reason || "—")}</td>
<td>${esc(b.banned_by || "—")}</td>
<td><button class="btn btn-xs btn-ghost" onclick="adminUnban('${ipEnc}')">Unban</button></td>
</tr>`;
}).join("");
} catch { /* silent */ }
}
// ═══════════════════════════════════════════════
// KEYBOARD
// ═══════════════════════════════════════════════