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 @@
-
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
// ═══════════════════════════════════════════════