// Dashboard rendering and data fetching functions for Fail2ban UI "use strict"; function refreshData(options) { options = options || {}; var enabledServers = serversCache.filter(function(s) { return s.enabled; }); var summaryPromise; if (!serversCache.length || !enabledServers.length || !currentServerId) { latestSummary = null; latestSummaryError = null; summaryPromise = Promise.resolve(); } else { summaryPromise = fetchSummaryData(); } if (!options.silent) { showLoading(true); } return Promise.all([ summaryPromise, fetchBanStatisticsData(), fetchBanEventsData(), fetchBanInsightsData() ]) .then(function() { renderDashboard(); }) .catch(function(err) { console.error('Error refreshing data:', err); latestSummaryError = err ? err.toString() : 'Unknown error'; renderDashboard(); }) .finally(function() { if (!options.silent) { showLoading(false); } }); } function fetchSummaryData() { return fetch(withServerParam('/api/summary')) .then(function(res) { return res.json(); }) .then(function(data) { if (data && !data.error) { latestSummary = data; latestSummaryError = null; } else { latestSummary = null; latestSummaryError = data && data.error ? data.error : t('dashboard.errors.summary_failed', 'Failed to load summary from server.'); } }) .catch(function(err) { latestSummary = null; latestSummaryError = err ? err.toString() : 'Unknown error'; }); } function fetchBanStatisticsData() { return fetch('/api/events/bans/stats') .then(function(res) { return res.json(); }) .then(function(data) { latestBanStats = data && data.counts ? data.counts : {}; }) .catch(function(err) { console.error('Error fetching ban statistics:', err); latestBanStats = latestBanStats || {}; }); } function fetchBanEventsData() { return fetch('/api/events/bans?limit=200') .then(function(res) { return res.json(); }) .then(function(data) { latestBanEvents = data && data.events ? data.events : []; // Track the last event ID to prevent duplicates from WebSocket if (latestBanEvents.length > 0 && wsManager) { wsManager.lastBanEventId = latestBanEvents[0].id; } }) .catch(function(err) { console.error('Error fetching ban events:', err); latestBanEvents = latestBanEvents || []; }); } // Add new ban event from WebSocket function addBanEventFromWebSocket(event) { // Check if event already exists (prevent duplicates) // Only check by ID if both events have IDs var exists = false; if (event.id) { exists = latestBanEvents.some(function(e) { return e.id === event.id; }); } else { // If no ID, check by IP, jail, and occurredAt timestamp exists = latestBanEvents.some(function(e) { return e.ip === event.ip && e.jail === event.jail && e.occurredAt === event.occurredAt; }); } if (!exists) { console.log('Adding new ban event from WebSocket:', event); // Prepend to the beginning of the array latestBanEvents.unshift(event); // Keep only the last 200 events if (latestBanEvents.length > 200) { latestBanEvents = latestBanEvents.slice(0, 200); } // Show toast notification first if (typeof showBanEventToast === 'function') { showBanEventToast(event); } // Refresh dashboard data (summary, stats, insights) and re-render refreshDashboardData(); } else { console.log('Skipping duplicate ban event:', event); } } // Refresh dashboard data when new ban event arrives via WebSocket function refreshDashboardData() { // Refresh ban statistics and insights in the background // Also refresh summary if we have a server selected var enabledServers = serversCache.filter(function(s) { return s.enabled; }); var summaryPromise; if (serversCache.length && enabledServers.length && currentServerId) { summaryPromise = fetchSummaryData(); } else { summaryPromise = Promise.resolve(); } Promise.all([ summaryPromise, fetchBanStatisticsData(), fetchBanInsightsData() ]).then(function() { // Re-render the dashboard to show updated stats renderDashboard(); }).catch(function(err) { console.error('Error refreshing dashboard data:', err); // Still re-render even if refresh fails renderDashboard(); }); } function fetchBanInsightsData() { var sevenDaysAgo = new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString(); var sinceQuery = '?since=' + encodeURIComponent(sevenDaysAgo); var globalPromise = fetch('/api/events/bans/insights' + sinceQuery) .then(function(res) { return res.json(); }) .then(function(data) { latestBanInsights = normalizeInsights(data); }) .catch(function(err) { console.error('Error fetching ban insights:', err); if (!latestBanInsights) { latestBanInsights = normalizeInsights(null); } }); var serverPromise; if (currentServerId) { serverPromise = fetch(withServerParam('/api/events/bans/insights' + sinceQuery)) .then(function(res) { return res.json(); }) .then(function(data) { latestServerInsights = normalizeInsights(data); }) .catch(function(err) { console.error('Error fetching server-specific ban insights:', err); latestServerInsights = null; }); } else { latestServerInsights = null; serverPromise = Promise.resolve(); } return Promise.all([globalPromise, serverPromise]); } function totalStoredBans() { if (latestBanInsights && latestBanInsights.totals && typeof latestBanInsights.totals.overall === 'number') { return latestBanInsights.totals.overall; } if (!latestBanStats) return 0; return Object.keys(latestBanStats).reduce(function(sum, key) { return sum + (latestBanStats[key] || 0); }, 0); } function totalBansToday() { if (latestBanInsights && latestBanInsights.totals && typeof latestBanInsights.totals.today === 'number') { return latestBanInsights.totals.today; } return 0; } function totalBansWeek() { if (latestBanInsights && latestBanInsights.totals && typeof latestBanInsights.totals.week === 'number') { return latestBanInsights.totals.week; } return 0; } function recurringIPsLastWeekCount() { var source = latestServerInsights || latestBanInsights; if (!source || !Array.isArray(source.recurring)) { return 0; } return source.recurring.length; } function getBanEventCountries() { var countries = {}; latestBanEvents.forEach(function(event) { var country = (event.country || '').trim(); var key = country.toLowerCase(); if (!countries[key]) { countries[key] = country; } }); var keys = Object.keys(countries); keys.sort(); return keys.map(function(key) { return countries[key]; }); } function getFilteredBanEvents() { var text = (banEventsFilterText || '').toLowerCase(); var countryFilter = (banEventsFilterCountry || '').toLowerCase(); return latestBanEvents.filter(function(event) { var matchesCountry = !countryFilter || countryFilter === 'all'; if (!matchesCountry) { var eventCountryValue = (event.country || '').toLowerCase(); if (!eventCountryValue) { eventCountryValue = '__unknown__'; } matchesCountry = eventCountryValue === countryFilter; } if (!text) { return matchesCountry; } var haystack = [ event.ip, event.jail, event.serverName, event.hostname, event.country ].map(function(value) { return (value || '').toLowerCase(); }); var matchesText = haystack.some(function(value) { return value.indexOf(text) !== -1; }); return matchesCountry && matchesText; }); } function scheduleLogOverviewRender() { if (banEventsFilterDebounce) { clearTimeout(banEventsFilterDebounce); } banEventsFilterDebounce = setTimeout(function() { renderLogOverviewSection(); banEventsFilterDebounce = null; }, 100); } function updateBanEventsSearch(value) { banEventsFilterText = value || ''; scheduleLogOverviewRender(); } function updateBanEventsCountry(value) { banEventsFilterCountry = value || 'all'; scheduleLogOverviewRender(); } function getRecurringIPMap() { var map = {}; if (latestBanInsights && Array.isArray(latestBanInsights.recurring)) { latestBanInsights.recurring.forEach(function(stat) { if (stat && stat.ip) { map[stat.ip] = stat; } }); } return map; } function renderBannedIPs(jailName, ips) { if (!ips || ips.length === 0) { return 'No banned IPs'; } 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 = '
'; function bannedIpRow(ip) { var safeIp = escapeHtml(ip); var encodedIp = encodeURIComponent(ip); return '' + '
' + ' ' + safeIp + '' + ' ' + '
'; } visible.forEach(function(ip) { content += bannedIpRow(ip); }); if (hidden.length) { content += ''; var moreLabel = t('dashboard.banned.show_more', 'Show more') + ' +' + hidden.length; var lessLabel = t('dashboard.banned.show_less', 'Hide extra'); content += '' + ''; } content += '
'; return content; } function filterIPs() { const input = document.getElementById("ipSearch"); if (!input) { return; } const query = input.value.trim(); const rows = document.querySelectorAll("#jailsTable .jail-row"); rows.forEach(row => { const hiddenSections = row.querySelectorAll(".banned-ip-hidden"); 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; ipItems.forEach(item => { const span = item.querySelector("span.text-sm"); if (!span) return; const storedValue = span.getAttribute("data-ip-value"); const originalIP = storedValue ? decodeURIComponent(storedValue) : span.textContent.trim(); if (query === "") { item.style.display = ""; span.textContent = originalIP; rowHasMatch = true; } else if (originalIP.indexOf(query) !== -1) { item.style.display = ""; span.innerHTML = highlightQueryMatch(originalIP, query); rowHasMatch = true; } else { item.style.display = "none"; } }); row.style.display = rowHasMatch ? "" : "none"; }); } 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"); } } function unbanIP(jail, ip) { const confirmMsg = isLOTRModeActive ? 'Restore ' + ip + ' to the realm from ' + jail + '?' : 'Unban IP ' + ip + ' from jail ' + jail + '?'; if (!confirm(confirmMsg)) { return; } showLoading(true); var url = '/api/jails/' + encodeURIComponent(jail) + '/unban/' + encodeURIComponent(ip); fetch(withServerParam(url), { method: 'POST', headers: serverHeaders() }) .then(function(res) { return res.json(); }) .then(function(data) { if (data.error) { showToast("Error unbanning IP: " + data.error, 'error'); } else { showToast(data.message || "IP unbanned successfully", 'success'); } return refreshData({ silent: true }); }) .catch(function(err) { showToast("Error: " + err, 'error'); }) .finally(function() { showLoading(false); }); } 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) { container.innerHTML = '' + ''; if (typeof updateTranslations === 'function') updateTranslations(); restoreFocusState(focusState); return; } if (!enabledServers.length) { container.innerHTML = '' + ''; if (typeof updateTranslations === 'function') updateTranslations(); restoreFocusState(focusState); return; } var summary = latestSummary; var html = ''; if (latestSummaryError) { html += '' + '
' + escapeHtml(latestSummaryError) + '
'; } if (!summary) { html += '' + '
' + '

Loading summary data…

' + '
'; } 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 recurringWeekCount = recurringIPsLastWeekCount(); html += '' + '
' + '
' + '

Active Jails

' + '

' + (summary.jails ? summary.jails.length : 0) + '

' + '
' + '
' + '

Total Banned IPs

' + '

' + totalBanned + '

' + '
' + '
' + '

New Last Hour

' + '

' + newLastHour + '

' + '
' + '
' + '

Recurring IPs (7 days)

' + '

' + recurringWeekCount + '

' + '

Keep an eye on repeated offenders across all servers.

' + '
' + '
'; html += '' + '
' + '
' + '
' + '

Overview active Jails and Blocks

' + '

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.

' + '
' + '
' + ' ' + ' ' + '
' + '
'; if (!summary.jails || summary.jails.length === 0) { html += '

No jails found.

'; } else { html += '' + '
' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' '; summary.jails.forEach(function(jail) { var bannedHTML = renderBannedIPs(jail.jailName, jail.bannedIPs || []); html += '' + '' + ' ' + ' ' + ' ' + ' ' + ''; }); html += '
JailBanned IPs
' + ' ' + escapeHtml(jail.jailName) + ' ' + ' ' + bannedHTML + '
'; html += '
'; } html += '
'; // close overview card } html += '
' + renderLogOverviewContent() + '
'; container.innerHTML = html; restoreFocusState(focusState); const extIpEl = document.getElementById('external-ip'); if (extIpEl) { extIpEl.addEventListener('click', function() { const ip = extIpEl.textContent.trim(); const searchInput = document.getElementById('ipSearch'); if (searchInput) { searchInput.value = ip; filterIPs(); searchInput.focus(); searchInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }); } filterIPs(); initializeSearch(); if (typeof updateTranslations === 'function') { updateTranslations(); } // Update LOTR terminology if active if (isLOTRModeActive) { updateDashboardLOTRTerminology(true); } } function renderLogOverviewSection() { var target = document.getElementById('logOverview'); if (!target) return; var focusState = captureFocusState(target); target.innerHTML = renderLogOverviewContent(); restoreFocusState(focusState); if (typeof updateTranslations === 'function') { updateTranslations(); } } function renderLogOverviewContent() { var html = '' + '
' + '
' + '
' + '

Internal Log Overview

' + '

Events stored by Fail2ban-UI across all connectors.

' + '
' + ' ' + '
'; var statsKeys = Object.keys(latestBanStats || {}); statsKeys.sort(function(a, b) { return (latestBanStats[b] || 0) - (latestBanStats[a] || 0); }); var totalStored = totalStoredBans(); var todayCount = totalBansToday(); var weekCount = totalBansWeek(); if (statsKeys.length === 0 && totalStored === 0) { html += '

No ban events recorded yet.

'; } else { html += '' + '
' + '
' + '
' + '
' + '

Total stored events

' + '

' + totalStored + '

' + '
' + ' ' + '
' + '
' + '
' + '

Today

' + '

' + todayCount + '

' + '
' + '
' + '

Last 7 days

' + '

' + weekCount + '

' + '
' + '
' + '
' + '
' + '

Events per server

' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' '; if (!statsKeys.length) { html += ''; } else { statsKeys.forEach(function(serverId) { var count = latestBanStats[serverId] || 0; var server = serversCache.find(function(s) { return s.id === serverId; }); html += '' + ' ' + ' ' + ' ' + ' '; }); } html += '
ServerCount
No per-server data available yet.
' + escapeHtml(server ? server.name : serverId) + '' + count + '
'; } html += '

Recent stored events

'; if (!latestBanEvents.length) { html += '

No stored events found.

'; } else { var countries = getBanEventCountries(); var filteredEvents = getFilteredBanEvents(); var recurringMap = getRecurringIPMap(); var searchQuery = (banEventsFilterText || '').trim(); html += '' + '
' + '
' + ' ' + ' ' + '
' + '
' + ' ' + ' ' + '
' + '
'; html += '

' + t('logs.overview.recent_count_label', 'Events shown') + ': ' + filteredEvents.length + ' / ' + latestBanEvents.length + '

'; if (!filteredEvents.length) { html += '

No stored events match the current filters.

'; } else { html += '' + '
' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' '; filteredEvents.forEach(function(event) { var index = latestBanEvents.indexOf(event); var hasWhois = event.whois && event.whois.trim().length > 0; var hasLogs = event.logs && event.logs.trim().length > 0; var serverValue = event.serverName || event.serverId || ''; var jailValue = event.jail || ''; var ipValue = event.ip || ''; var serverCell = highlightQueryMatch(serverValue, searchQuery); var jailCell = highlightQueryMatch(jailValue, searchQuery); var ipCell = highlightQueryMatch(ipValue, searchQuery); if (event.ip && recurringMap[event.ip]) { ipCell += ' ' + t('logs.badge.recurring', 'Recurring') + ''; } html += '' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' '; }); html += '
TimeServerIPActions
' + escapeHtml(formatDateTime(event.occurredAt || event.createdAt)) + '' + serverCell + '' + ipCell + '' + '
' + (hasWhois ? ' ' : ' ') + (hasLogs ? ' ' : ' ') + '
' + '
'; } } html += '
'; return html; }