"use strict"; // Dashboard data fetching and rendering. // ========================================================================= // Data Fetching // ========================================================================= 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 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 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 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 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; } }); } // ========================================================================= // Triggers Ban / Unban Actions from the dashboard // ========================================================================= // Sends request to ban an IP in a jail. function banIP(jail, ip) { const confirmMsg = isLOTRModeActive ? 'Banish ' + ip + ' from the realm in ' + jail + '?' : 'Block IP ' + ip + ' in jail ' + jail + '?'; if (!confirm(confirmMsg)) { return; } showLoading(true); var url = '/api/jails/' + encodeURIComponent(jail) + '/ban/' + encodeURIComponent(ip); fetch(withServerParam(url), { method: 'POST', headers: serverHeaders() }) .then(function(res) { return res.json(); }) .then(function(data) { if (data.error) { showToast("Error blocking IP: " + data.error, 'error'); } else { showToast(t('dashboard.manual_block.success', 'IP blocked successfully'), 'success'); return refreshData({ silent: true }); } }) .catch(function(err) { showToast("Error: " + err, 'error'); }) .finally(function() { showLoading(false); }); } // Sends request to unban an IP from a jail. 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'); } return refreshData({ silent: true }); }) .catch(function(err) { showToast("Error: " + err, 'error'); }) .finally(function() { showLoading(false); }); } // ========================================================================= // Main Dashboard Rendering Function // ========================================================================= // Rendering the upper part of the dashboard. 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 = ''; // Persistent warning banner when jail.local is not managed by Fail2ban-UI if (jailLocalWarning) { html += '' + ''; } if (latestSummaryError) { html += '' + '
' + escapeHtml(latestSummaryError) + '
'; } // If there is no summary data, we show a loading message if (!summary) { html += '' + '
' + '

Loading summary data…

' + '
'; } else { // If there is "summary data", we render the complete upper part of the dashboard here. 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.

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

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 += '
'; } if (summary && summary.jails && summary.jails.length > 0) { var enabledJails = summary.jails.filter(function(j) { return j.enabled !== false; }); if (enabledJails.length > 0) { // Rendering the manual ban-block from the dashboard here html += '' + '
' + '
' + '
' + '
' + '

Manual Block IP

' + '

Manually block an IP address in a specific jail.

' + '

Click to expand and block an IP address

' + '
' + '
' + ' ' + '
' + '
' + '
' + ' ' + '
'; } } 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(); } if (isLOTRModeActive) { updateDashboardLOTRTerminology(true); } } // ========================================================================= // Rendering the colapsable "Banned IPs per jail" section // ========================================================================= 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; } // ========================================================================= // Internal Log Overview Section Functions // ========================================================================= 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

' + '' + '
'; var countries = getBanEventCountries(); var recurringMap = getRecurringIPMap(); var searchQuery = (banEventsFilterText || '').trim(); var totalLabel = banEventsTotal != null ? banEventsTotal : '—'; // Rendering the search and filter options for the recent stored events html += '' + '
' + '
' + ' ' + ' ' + '
' + '
' + ' ' + ' ' + '
' + '
' + '

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

' + '
' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' '; if (!latestBanEvents.length) { var hasFilter = (banEventsFilterText || '').trim().length > 0 || ((banEventsFilterCountry || 'all').trim() !== 'all'); var emptyMsgKey = hasFilter ? 'logs.overview.recent_filtered_empty' : 'logs.overview.recent_empty'; html += ''; } else { latestBanEvents.forEach(function(event, index) { 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') + ''; } var eventType = event.eventType || 'ban'; var eventTypeBadge = ''; if (eventType === 'unban') { eventTypeBadge = ' Unban'; } else { eventTypeBadge = ' Ban'; } html += '' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' '; }); } html += '
TimeServerIPActions
' + escapeHtml(formatDateTime(event.occurredAt || event.createdAt)) + '' + serverCell + '' + ipCell + eventTypeBadge + '' + '
' + (hasWhois ? ' ' : ' ') + (hasLogs ? ' ' : ' ') + '
' + '
'; if (banEventsHasMore && latestBanEvents.length > 0 && latestBanEvents.length < BAN_EVENTS_MAX_LOADED) { var loadMoreLabel = typeof t === 'function' ? t('logs.overview.load_more', 'Load more') : 'Load more'; html += '
' + '' + '
'; } html += '
'; return html; } // ========================================================================= // Search and Filtering Functions // ========================================================================= 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 clearStoredBanEvents() { var msg = t('logs.overview.clear_events_confirm', 'This will permanently delete all stored ban events. Statistics, insights, and the event history will be reset to zero.\n\nThis action cannot be undone. Continue?'); if (!confirm(msg)) return; fetch('/api/events/bans', { method: 'DELETE', headers: serverHeaders() }) .then(function(res) { return res.json(); }) .then(function(data) { if (data.error) { showToast(data.error, 'error'); return; } showToast(t('logs.overview.clear_events_success', 'All stored ban events cleared.'), 'success'); latestBanEvents = []; latestBanStats = {}; latestBanInsights = null; banEventsTotal = 0; banEventsHasMore = false; renderLogOverviewSection(); }) .catch(function(err) { showToast(String(err), 'error'); }); } // Filtering function for the banned IPs for the dashboard. 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"; }); } // ========================================================================= // Helper Functions // ========================================================================= // Helper function to toggle the banned list section for the dashboard. 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"); } } // Helper function to toggle the manual block section for the dashboard. function toggleManualBlockSection() { var container = document.getElementById('manualBlockFormContainer'); var icon = document.getElementById('manualBlockToggleIcon'); if (!container || !icon) { return; } var isHidden = container.classList.contains("hidden"); if (isHidden) { container.classList.remove("hidden"); icon.classList.remove("fa-chevron-down"); icon.classList.add("fa-chevron-up"); } else { container.classList.add("hidden"); icon.classList.remove("fa-chevron-up"); icon.classList.add("fa-chevron-down"); } } // This handles manual block actions and calls the banIP function. function handleManualBlock() { var jailSelect = document.getElementById('blockJailSelect'); var ipInput = document.getElementById('blockIPInput'); if (!jailSelect || !ipInput) { return; } var jail = jailSelect.value; var ip = ipInput.value.trim(); if (!jail) { showToast(t('dashboard.manual_block.jail_required', 'Please select a jail'), 'error'); jailSelect.focus(); return; } if (!ip) { showToast(t('dashboard.manual_block.ip_required', 'Please enter an IP address'), 'error'); ipInput.focus(); return; } // IPv4 / IPv6 validation var ipv4Pattern = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/; var ipv6Pattern = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/; if (!ipv4Pattern.test(ip) && !ipv6Pattern.test(ip)) { showToast(t('dashboard.manual_block.invalid_ip', 'Please enter a valid IP address'), 'error'); ipInput.focus(); return; } banIP(jail, ip); ipInput.value = ''; jailSelect.value = ''; } // Helper function to add the "Internal Log Overview" content to the dashboard. 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 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('&'); } // Helper function to add a new ban event from the WebSocket to the dashboard. function addBanEventFromWebSocket(event) { var hasSearch = (banEventsFilterText || '').trim().length > 0; if (hasSearch) { 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); } } // Helper function to refresh the dashboard data by fetching the summary and ban insights. function refreshDashboardData() { 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() { renderDashboard(); }).catch(function(err) { console.error('Error refreshing dashboard data:', err); renderDashboard(); }); } // Helper functions to query the total number of banned IPs 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); } // Helper functions to query the total number of banned IPs of today. function totalBansToday() { if (latestBanInsights && latestBanInsights.totals && typeof latestBanInsights.totals.today === 'number') { return latestBanInsights.totals.today; } return 0; } // Helper functions to query the total number of banned IPs of last week. function totalBansWeek() { if (latestBanInsights && latestBanInsights.totals && typeof latestBanInsights.totals.week === 'number') { return latestBanInsights.totals.week; } return 0; } // Helper functions to query the total number of recurring IPs of last week. function recurringIPsLastWeekCount() { var source = latestServerInsights || latestBanInsights; if (!source || !Array.isArray(source.recurring)) { return 0; } return source.recurring.length; } // Helper functions to query the countries of the banned IPs. 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]; }); } // Helper functions to schedule the refetch of the banned events. function scheduleBanEventsRefetch() { if (banEventsFilterDebounce) { clearTimeout(banEventsFilterDebounce); } banEventsFilterDebounce = setTimeout(function() { banEventsFilterDebounce = null; fetchBanEventsData().then(function() { renderLogOverviewSection(); }); }, 300); } // Helper functions to query the recurring IPs of the banned IPs. 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; }