Make current bans per jail colapsable

This commit is contained in:
2025-11-17 16:29:04 +01:00
parent 3af93f3237
commit b1bdb66516

View File

@@ -1410,6 +1410,66 @@
return 0; return 0;
} }
function recurringIPsLastWeekCount() {
if (!latestBanInsights || !Array.isArray(latestBanInsights.recurring)) {
return 0;
}
var cutoff = Date.now() - (7 * 24 * 60 * 60 * 1000);
var seen = {};
var count = 0;
latestBanInsights.recurring.forEach(function(stat) {
if (!stat || !stat.ip) {
return;
}
var lastSeenTime = stat.lastSeen ? new Date(stat.lastSeen).getTime() : NaN;
if (isNaN(lastSeenTime) || lastSeenTime >= cutoff) {
if (!seen[stat.ip]) {
seen[stat.ip] = true;
count += 1;
}
}
});
return count;
}
function captureFocusState(container) {
var active = document.activeElement;
if (!active || !container || !container.contains(active)) {
return null;
}
var state = { id: active.id || null };
if (!state.id) {
return null;
}
try {
if (typeof active.selectionStart === 'number' && typeof active.selectionEnd === 'number') {
state.selectionStart = active.selectionStart;
state.selectionEnd = active.selectionEnd;
}
} catch (err) {
// Ignore selection errors for elements that do not support it.
}
return state;
}
function restoreFocusState(state) {
if (!state || !state.id) {
return;
}
var next = document.getElementById(state.id);
if (!next) {
return;
}
next.focus();
try {
if (typeof state.selectionStart === 'number' && typeof state.selectionEnd === 'number' && typeof next.setSelectionRange === 'function') {
next.setSelectionRange(state.selectionStart, state.selectionEnd);
}
} catch (err) {
// Element may not support setSelectionRange; ignore.
}
}
function getBanEventCountries() { function getBanEventCountries() {
var countries = {}; var countries = {};
latestBanEvents.forEach(function(event) { latestBanEvents.forEach(function(event) {
@@ -1492,6 +1552,7 @@
function renderDashboard() { function renderDashboard() {
var container = document.getElementById('dashboard'); var container = document.getElementById('dashboard');
if (!container) return; if (!container) return;
var focusState = captureFocusState(container);
var enabledServers = serversCache.filter(function(s) { return s.enabled; }); var enabledServers = serversCache.filter(function(s) { return s.enabled; });
if (!serversCache.length) { if (!serversCache.length) {
@@ -1531,7 +1592,7 @@
} else { } else {
var totalBanned = summary.jails ? summary.jails.reduce(function(sum, j) { return sum + (j.totalBanned || 0); }, 0) : 0; var totalBanned = summary.jails ? summary.jails.reduce(function(sum, j) { return sum + (j.totalBanned || 0); }, 0) : 0;
var newLastHour = summary.jails ? summary.jails.reduce(function(sum, j) { return sum + (j.newInLastHour || 0); }, 0) : 0; var newLastHour = summary.jails ? summary.jails.reduce(function(sum, j) { return sum + (j.newInLastHour || 0); }, 0) : 0;
var totalStored = totalStoredBans(); var recurringWeekCount = recurringIPsLastWeekCount();
html += '' html += ''
+ '<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">' + '<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">'
@@ -1548,8 +1609,9 @@
+ ' <p class="text-2xl font-semibold text-gray-800">' + newLastHour + '</p>' + ' <p class="text-2xl font-semibold text-gray-800">' + newLastHour + '</p>'
+ ' </div>' + ' </div>'
+ ' <div class="bg-white rounded-lg shadow p-4">' + ' <div class="bg-white rounded-lg shadow p-4">'
+ ' <p class="text-sm text-gray-500" data-i18n="dashboard.cards.total_logged">Stored Ban Events</p>' + ' <p class="text-sm text-gray-500" data-i18n="dashboard.cards.recurring_week">Recurring IPs (7 days)</p>'
+ ' <p class="text-2xl font-semibold text-gray-800">' + totalStored + '</p>' + ' <p class="text-2xl font-semibold text-gray-800">' + recurringWeekCount + '</p>'
+ ' <p class="text-xs text-gray-500 mt-1" data-i18n="dashboard.cards.recurring_hint">Keep an eye on repeated offenders across all servers.</p>'
+ ' </div>' + ' </div>'
+ '</div>'; + '</div>';
@@ -1559,6 +1621,7 @@
+ ' <div>' + ' <div>'
+ ' <h3 class="text-lg font-medium text-gray-900 mb-2" data-i18n="dashboard.overview">Overview active Jails and Blocks</h3>' + ' <h3 class="text-lg font-medium text-gray-900 mb-2" data-i18n="dashboard.overview">Overview active Jails and Blocks</h3>'
+ ' <p class="text-sm text-gray-500" data-i18n="dashboard.overview_hint">Use the search to filter banned IPs and click a jail to edit its configuration.</p>' + ' <p class="text-sm text-gray-500" data-i18n="dashboard.overview_hint">Use the search to filter banned IPs and click a jail to edit its configuration.</p>'
+ ' <p class="text-sm text-gray-500 mt-1" data-i18n="dashboard.overview_detail">Collapse or expand long lists to quickly focus on impacted services.</p>'
+ ' </div>' + ' </div>'
+ ' <div>' + ' <div>'
+ ' <label for="ipSearch" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="dashboard.search_label">Search Banned IPs</label>' + ' <label for="ipSearch" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="dashboard.search_label">Search Banned IPs</label>'
@@ -1607,6 +1670,7 @@
html += renderLogOverview(); html += renderLogOverview();
container.innerHTML = html; container.innerHTML = html;
restoreFocusState(focusState);
const extIpEl = document.getElementById('external-ip'); const extIpEl = document.getElementById('external-ip');
if (extIpEl) { if (extIpEl) {
@@ -2188,21 +2252,72 @@
} }
// Render banned IPs with "Unban" button // Render banned IPs with "Unban" button
function slugifyId(value, prefix) {
var input = (value || '').toString();
var base = input.toLowerCase().replace(/[^a-z0-9]+/g, '-');
var hash = 0;
for (var i = 0; i < input.length; i++) {
hash = ((hash << 5) - hash) + input.charCodeAt(i);
hash |= 0;
}
hash = Math.abs(hash);
base = base.replace(/^-+|-+$/g, '');
if (!base) {
base = 'item';
}
return (prefix || 'id') + '-' + base + '-' + hash;
}
function renderBannedIPs(jailName, ips) { function renderBannedIPs(jailName, ips) {
if (!ips || ips.length === 0) { if (!ips || ips.length === 0) {
return '<em class="text-gray-500" data-i18n="dashboard.no_banned_ips">No banned IPs</em>'; return '<em class="text-gray-500" data-i18n="dashboard.no_banned_ips">No banned IPs</em>';
} }
var listId = slugifyId(jailName || 'jail', 'banned-list');
var hiddenId = listId + '-hidden';
var toggleId = listId + '-toggle';
var maxVisible = 5;
var visible = ips.slice(0, maxVisible);
var hidden = ips.slice(maxVisible);
var content = '<div class="space-y-2">'; var content = '<div class="space-y-2">';
ips.forEach(function(ip) {
content += '' function bannedIpRow(ip) {
+ '<div class="flex items-center justify-between">' var safeIp = escapeHtml(ip);
+ ' <span class="text-sm">' + ip + '</span>' var encodedIp = encodeURIComponent(ip);
return ''
+ '<div class="flex items-center justify-between banned-ip-item" data-ip="' + safeIp + '">'
+ ' <span class="text-sm" data-ip-value="' + encodedIp + '">' + safeIp + '</span>'
+ ' <button class="bg-yellow-500 text-white px-3 py-1 rounded text-sm hover:bg-yellow-600 transition-colors"' + ' <button class="bg-yellow-500 text-white px-3 py-1 rounded text-sm hover:bg-yellow-600 transition-colors"'
+ ' onclick="unbanIP(\'' + jailName + '\', \'' + ip + '\')">' + ' onclick="unbanIP(\'' + jailName + '\', \'' + ip + '\')">'
+ ' <span data-i18n="dashboard.unban">Unban</span>' + ' <span data-i18n="dashboard.unban">Unban</span>'
+ ' </button>' + ' </button>'
+ '</div>'; + '</div>';
}
visible.forEach(function(ip) {
content += bannedIpRow(ip);
}); });
if (hidden.length) {
content += '<div class="space-y-2 mt-2 hidden banned-ip-hidden" id="' + hiddenId + '" data-initially-hidden="true">';
hidden.forEach(function(ip) {
content += bannedIpRow(ip);
});
content += '</div>';
var moreLabel = t('dashboard.banned.show_more', 'Show more') + ' +' + hidden.length;
var lessLabel = t('dashboard.banned.show_less', 'Hide extra');
content += ''
+ '<button type="button" class="text-xs font-semibold text-blue-600 hover:text-blue-800 banned-ip-toggle"'
+ ' id="' + toggleId + '"'
+ ' data-target="' + hiddenId + '"'
+ ' data-more-label="' + escapeHtml(moreLabel) + '"'
+ ' data-less-label="' + escapeHtml(lessLabel) + '"'
+ ' data-expanded="false"'
+ ' onclick="toggleBannedList(\'' + hiddenId + '\', \'' + toggleId + '\')">'
+ escapeHtml(moreLabel)
+ '</button>';
}
content += '</div>'; content += '</div>';
return content; return content;
} }
@@ -2210,37 +2325,70 @@
// Filter IPs on dashboard table // Filter IPs on dashboard table
function filterIPs() { function filterIPs() {
const input = document.getElementById("ipSearch"); const input = document.getElementById("ipSearch");
if (!input) {
return;
}
const query = input.value.trim(); const query = input.value.trim();
const rows = document.querySelectorAll("#jailsTable .jail-row");
// Process each row in the jails table rows.forEach(row => {
document.querySelectorAll("#jailsTable .jail-row").forEach(row => { const hiddenSections = row.querySelectorAll(".banned-ip-hidden");
const ipItems = row.querySelectorAll("div.flex"); const toggleButtons = row.querySelectorAll(".banned-ip-toggle");
if (query === "") {
hiddenSections.forEach(section => {
if (section.getAttribute("data-initially-hidden") === "true") {
section.classList.add("hidden");
}
});
toggleButtons.forEach(button => {
const moreLabel = button.getAttribute("data-more-label");
if (moreLabel) {
button.textContent = moreLabel;
}
button.setAttribute("data-expanded", "false");
});
} else {
hiddenSections.forEach(section => section.classList.remove("hidden"));
toggleButtons.forEach(button => {
const lessLabel = button.getAttribute("data-less-label");
if (lessLabel) {
button.textContent = lessLabel;
}
button.setAttribute("data-expanded", "true");
});
}
const ipItems = row.querySelectorAll(".banned-ip-item");
let rowHasMatch = false; let rowHasMatch = false;
ipItems.forEach(li => { ipItems.forEach(item => {
const span = li.querySelector("span.text-sm"); const span = item.querySelector("span.text-sm");
if (!span) return; if (!span) return;
// stash original if needed const storedValue = span.getAttribute("data-ip-value");
let originalIP = span.getAttribute("data-original"); const originalIP = storedValue ? decodeURIComponent(storedValue) : span.textContent.trim();
if (!originalIP) {
originalIP = span.textContent.trim();
span.setAttribute("data-original", originalIP);
}
if (query === "") { if (query === "") {
// When the search query is empty, show all IP items and reset their inner HTML. // When the search query is empty, show all IP items and reset their inner HTML.
li.style.display = ""; item.style.display = "";
span.innerHTML = originalIP; span.textContent = originalIP;
rowHasMatch = true; rowHasMatch = true;
} else if (originalIP.includes(query)) { } else if (originalIP.indexOf(query) !== -1) {
// If the IP contains the query, show the item and highlight the matching text. // If the IP contains the query, show the item and highlight the matching text.
li.style.display = ""; item.style.display = "";
const regex = new RegExp(query, "gi"); const escapedPattern = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
span.innerHTML = originalIP.replace(regex, m => `<mark>${m}</mark>`); const regex = new RegExp(escapedPattern, "gi");
var highlighted = originalIP.replace(regex, function(match) {
return "%%MARK_START%%" + match + "%%MARK_END%%";
});
var safeHighlighted = escapeHtml(highlighted)
.replace(/%%MARK_START%%/g, "<mark>")
.replace(/%%MARK_END%%/g, "</mark>");
span.innerHTML = safeHighlighted;
rowHasMatch = true; rowHasMatch = true;
} else { } else {
li.style.display = "none"; item.style.display = "none";
} }
}); });
@@ -2249,6 +2397,24 @@
}); });
} }
function toggleBannedList(hiddenId, buttonId) {
var hidden = document.getElementById(hiddenId);
var button = document.getElementById(buttonId);
if (!hidden || !button) {
return;
}
var isHidden = hidden.classList.contains("hidden");
if (isHidden) {
hidden.classList.remove("hidden");
button.textContent = button.getAttribute("data-less-label") || button.textContent;
button.setAttribute("data-expanded", "true");
} else {
hidden.classList.add("hidden");
button.textContent = button.getAttribute("data-more-label") || button.textContent;
button.setAttribute("data-expanded", "false");
}
}
//******************************************************************* //*******************************************************************
//* Functions to manage IP-bans : * //* Functions to manage IP-bans : *
//******************************************************************* //*******************************************************************