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);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.device-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.confirm-text {
|
.confirm-text {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
@@ -675,12 +680,31 @@
|
|||||||
<th>Requests</th>
|
<th>Requests</th>
|
||||||
<th>Last Seen</th>
|
<th>Last Seen</th>
|
||||||
<th>User Agent</th>
|
<th>User Agent</th>
|
||||||
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="debug-devices-tbody"></tbody>
|
<tbody id="debug-devices-tbody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -903,7 +927,7 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Cache TTL (seconds)</label>
|
<label>Cache TTL (seconds)</label>
|
||||||
<input type="number" id="view-cache-ttl" value="${cacheTtl}" min="0" max="86400" required>
|
<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>
|
||||||
|
|
||||||
<div class="field" style="display:flex;gap:12px;">
|
<div class="field" style="display:flex;gap:12px;">
|
||||||
@@ -1164,6 +1188,7 @@
|
|||||||
document.getElementById("debug-devices-tbody").innerHTML = data.devices
|
document.getElementById("debug-devices-tbody").innerHTML = data.devices
|
||||||
.map((d) => {
|
.map((d) => {
|
||||||
const ago = Math.round((Date.now() - d.lastSeen) / 1000);
|
const ago = Math.round((Date.now() - d.lastSeen) / 1000);
|
||||||
|
const ipEnc = encodeURIComponent(d.ip);
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${esc(d.ip)}</td>
|
<td>${esc(d.ip)}</td>
|
||||||
@@ -1171,10 +1196,20 @@
|
|||||||
<td>${d.requestCount}</td>
|
<td>${d.requestCount}</td>
|
||||||
<td>${ago}s ago</td>
|
<td>${ago}s ago</td>
|
||||||
<td class="url-cell" title="${esc(d.userAgent)}">${esc(d.userAgent.slice(0, 60))}</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>`;
|
</tr>`;
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
|
// ── Banned IPs table ──
|
||||||
|
fetchBannedIPs();
|
||||||
|
|
||||||
if (!debugTickInterval) {
|
if (!debugTickInterval) {
|
||||||
debugTickInterval = setInterval(tickDebugValues, 500);
|
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
|
// KEYBOARD
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user