"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 = ''
+ '
'
+ '
No Fail2ban servers configured
'
+ '
Add a server to start monitoring and controlling Fail2ban instances.
'
+ '
';
if (typeof updateTranslations === 'function') updateTranslations();
restoreFocusState(focusState);
return;
}
if (!enabledServers.length) {
container.innerHTML = ''
+ '
'
+ '
No active connectors
'
+ '
Enable the local connector or register a remote Fail2ban server to see live data.
'
+ '
';
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 += ''
+ '
'
+ ' '
+ '
'
+ '
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.
'
+ '
'
+ '
';
}
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 += ''
+ '
';
}
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 ''
+ '
';
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 += ''
+ '