diff --git a/pkg/web/templates/index.html b/pkg/web/templates/index.html index 5cad073..92d91b6 100644 --- a/pkg/web/templates/index.html +++ b/pkg/web/templates/index.html @@ -1410,6 +1410,66 @@ 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() { var countries = {}; latestBanEvents.forEach(function(event) { @@ -1492,6 +1552,7 @@ function renderDashboard() { var container = document.getElementById('dashboard'); if (!container) return; + var focusState = captureFocusState(container); var enabledServers = serversCache.filter(function(s) { return s.enabled; }); if (!serversCache.length) { @@ -1531,7 +1592,7 @@ } else { 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 totalStored = totalStoredBans(); + var recurringWeekCount = recurringIPsLastWeekCount(); html += '' + '
' + newLastHour + '
' + 'Stored Ban Events
' - + '' + totalStored + '
' + + 'Recurring IPs (7 days)
' + + '' + recurringWeekCount + '
' + + 'Keep an eye on repeated offenders across all servers.
' + 'Use the search to filter banned IPs and click a jail to edit its configuration.
' + + 'Collapse or expand long lists to quickly focus on impacted services.
' + '