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:
+118
-1
@@ -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
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user