// 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; jailLocalWarning = !!data.jailLocalWarning; } else { latestSummary = null; latestSummaryError = data && data.error ? data.error : t('dashboard.errors.summary_failed', 'Failed to load summary from server.'); jailLocalWarning = false; } }) .catch(function(err) { latestSummary = null; latestSummaryError = err ? err.toString() : 'Unknown error'; jailLocalWarning = false; }); } 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 || {}; }); } // Builds query string for ban events API: limit, offset, search, country, serverId function buildBanEventsQuery(offset, append) { var params = [ 'limit=' + BAN_EVENTS_PAGE_SIZE, 'offset=' + (append ? Math.min(latestBanEvents.length, BAN_EVENTS_MAX_LOADED) : 0) ]; var search = (banEventsFilterText || '').trim(); if (search) { params.push('search=' + encodeURIComponent(search)); } var country = (banEventsFilterCountry || 'all').trim(); if (country && country !== 'all') { params.push('country=' + encodeURIComponent(country)); } if (currentServerId) { params.push('serverId=' + encodeURIComponent(currentServerId)); } return '/api/events/bans?' + params.join('&'); } // options: { append: true } to load next page and append; otherwise fetches first page (reset). function fetchBanEventsData(options) { options = options || {}; var append = options.append === true; var offset = append ? Math.min(latestBanEvents.length, BAN_EVENTS_MAX_LOADED) : 0; if (append && offset >= BAN_EVENTS_MAX_LOADED) { return Promise.resolve(); } var url = buildBanEventsQuery(offset, append); return fetch(url) .then(function(res) { return res.json(); }) .then(function(data) { var events = data && data.events ? data.events : []; if (append) { latestBanEvents = latestBanEvents.concat(events); } else { latestBanEvents = events; } banEventsHasMore = data.hasMore === true; if (offset === 0 && typeof data.total === 'number') { banEventsTotal = data.total; } if (!append && latestBanEvents.length > 0 && wsManager) { wsManager.lastBanEventId = latestBanEvents[0].id; } }) .catch(function(err) { console.error('Error fetching ban events:', err); if (!append) { latestBanEvents = latestBanEvents || []; banEventsTotal = null; banEventsHasMore = false; } }); } // Add new ban or unban event from WebSocket (only when not searching; cap at BAN_EVENTS_MAX_LOADED) function addBanEventFromWebSocket(event) { var hasSearch = (banEventsFilterText || '').trim().length > 0; if (hasSearch) { // When user is searching, list is from API; don't prepend to avoid inconsistency if (typeof showBanEventToast === 'function') { showBanEventToast(event); } refreshDashboardData(); return; } var exists = false; if (event.id) { exists = latestBanEvents.some(function(e) { return e.id === event.id; }); } else { exists = latestBanEvents.some(function(e) { return e.ip === event.ip && e.jail === event.jail && e.eventType === event.eventType && e.occurredAt === event.occurredAt; }); } if (!exists) { if (!event.eventType) { event.eventType = 'ban'; } console.log('Adding new event from WebSocket:', event); latestBanEvents.unshift(event); if (latestBanEvents.length > BAN_EVENTS_MAX_LOADED) { latestBanEvents = latestBanEvents.slice(0, BAN_EVENTS_MAX_LOADED); } if (typeof showBanEventToast === 'function') { showBanEventToast(event); } 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]; }); } // Debounced refetch of ban events from API (search/country) and re-render only the log overview (no full dashboard = no scroll jump) function scheduleBanEventsRefetch() { if (banEventsFilterDebounce) { clearTimeout(banEventsFilterDebounce); } banEventsFilterDebounce = setTimeout(function() { banEventsFilterDebounce = null; fetchBanEventsData().then(function() { renderLogOverviewSection(); }); }, 300); } function updateBanEventsSearch(value) { banEventsFilterText = value || ''; scheduleBanEventsRefetch(); } function updateBanEventsCountry(value) { banEventsFilterCountry = value || 'all'; fetchBanEventsData().then(function() { renderLogOverviewSection(); }); } function loadMoreBanEvents() { if (latestBanEvents.length >= BAN_EVENTS_MAX_LOADED || !banEventsHasMore) { return; } fetchBanEventsData({ append: true }).then(function() { renderLogOverviewSection(); }); } 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.
' + 'jail.local not managed by Fail2ban-UI
' + 'The file /etc/fail2ban/jail.local on the selected server exists but is not managed by Fail2ban-UI. The callback action (ui-custom-action) is missing, which means ban/unban events will not be recorded and no email alerts will be sent. To fix this, move each jail section from jail.local into its own file under /etc/fail2ban/jail.d/ (use jailname.conf to keep a default or jailname.local to override an existing .conf). Then delete jail.local so Fail2ban-UI can create its own managed version. Ensure Fail2ban-UI has write permissions to /etc/fail2ban/ — see the documentation for details.
' + '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 + ' | ' + '
Manually block an IP address in a specific jail.
' + 'Click to expand and block an IP address
' + '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 + ' | ' + '
' + t('logs.overview.recent_count_label', 'Events shown') + ': ' + latestBanEvents.length + ' / ' + totalLabel + '
'; html += '' + '| Time | ' + 'Server | ' + 'Jail | ' + 'IP | ' + 'Country | ' + 'Actions | ' + '
|---|---|---|---|---|---|
| ' + escapeHtml(formatDateTime(event.occurredAt || event.createdAt)) + ' | ' + '' + serverCell + ' | ' + '' + jailCell + ' | ' + '' + ipCell + eventTypeBadge + ' | ' + '' + escapeHtml(event.country || '—') + ' | ' + ''
+ ' '
+ (hasWhois ? ' ' : ' ')
+ (hasLogs ? ' ' : ' ')
+ ' '
+ ' | '
+ '