// 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 or unban 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, eventType, and occurredAt timestamp exists = latestBanEvents.some(function(e) { return e.ip === event.ip && e.jail === event.jail && e.eventType === event.eventType && e.occurredAt === event.occurredAt; }); } if (!exists) { // Ensure eventType is set (default to 'ban' for backward compatibility) if (!event.eventType) { event.eventType = 'ban'; } console.log('Adding new 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 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 = '
No Fail2ban servers configured
' + 'Add a server to start monitoring and controlling Fail2ban instances.
' + 'No active connectors
' + 'Enable the local connector or register a remote Fail2ban server to see live data.
' + 'Loading summary data…
' + '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.
' + '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.
' + 'No jails found.
'; } else { html += '' + '| Jail | ' + 'Total Banned | ' + 'New Last Hour | ' + 'Banned IPs | ' + '
|---|---|---|---|
| ' + ' ' + escapeHtml(jail.jailName) + ' ' + ' | ' + '' + (jail.totalBanned || 0) + ' | ' + '' + (jail.newInLastHour || 0) + ' | ' + '' + bannedHTML + ' | ' + '
Events stored by Fail2ban-UI across all connectors.
' + 'No ban events recorded yet.
'; } else { html += '' + 'Total stored events
' + '' + totalStored + '
' + 'Today
' + '' + todayCount + '
' + 'Last 7 days
' + '' + weekCount + '
' + 'Events per server
' + '| Server | ' + 'Count | ' + '
|---|---|
| No per-server data available yet. | |
| ' + escapeHtml(server ? server.name : serverId) + ' | ' + '' + count + ' | ' + '
No stored events found.
'; } else { var countries = getBanEventCountries(); var filteredEvents = getFilteredBanEvents(); var recurringMap = getRecurringIPMap(); var searchQuery = (banEventsFilterText || '').trim(); 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 += '' + '| Time | ' + 'Server | ' + 'Jail | ' + 'IP | ' + 'Country | ' + 'Actions | ' + '
|---|---|---|---|---|---|
| ' + escapeHtml(formatDateTime(event.occurredAt || event.createdAt)) + ' | ' + '' + serverCell + ' | ' + '' + jailCell + ' | ' + '' + ipCell + eventTypeBadge + ' | ' + '' + escapeHtml(event.country || '—') + ' | ' + ''
+ ' '
+ (hasWhois ? ' ' : ' ')
+ (hasLogs ? ' ' : ' ')
+ ' '
+ ' | '
+ '