From ac759a60fe3088608fc10ec9e4e4d6f5ca7d291d Mon Sep 17 00:00:00 2001 From: Igor Barcik Date: Wed, 13 May 2026 13:36:37 +0200 Subject: [PATCH] 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 --- public/index.html | 119 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index ef3a645..99cbda4 100644 --- a/public/index.html +++ b/public/index.html @@ -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 @@ Requests Last Seen User Agent + Actions + +
+

🚫 Banned IPs

+
+ + + + + + + + + + + +
IPBanned AtReasonBanned ByActions
No banned IPs
+
+
@@ -903,7 +927,7 @@
-
How long a render stays in cache. 0 = always re-render. Each access extends TTL.
+
How long a render stays in cache. 0 = always re-render.
@@ -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 ` ${esc(d.ip)} @@ -1171,10 +1196,20 @@ ${d.requestCount} ${ago}s ago ${esc(d.userAgent.slice(0, 60))} + +
+ + + +
+ `; }) .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(` +

Ban IP

+
Ban ${esc(ipDisplay)}? Future connections will receive 403.
+
+ + +
+ `); + } + + 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 = 'No banned IPs'; + return; + } + tbody.innerHTML = banned.map((b) => { + const ipEnc = encodeURIComponent(b.ip); + const dt = new Date(b.banned_at * 1000).toLocaleString(); + return ` + + ${esc(b.ip)} + ${esc(dt)} + ${esc(b.reason || "—")} + ${esc(b.banned_by || "—")} + + `; + }).join(""); + } catch { /* silent */ } + } + // ═══════════════════════════════════════════════ // KEYBOARD // ═══════════════════════════════════════════════