// Server management functions for Fail2ban UI "use strict"; function loadServers() { return fetch('/api/servers') .then(function(res) { return res.json(); }) .then(function(data) { serversCache = data.servers || []; var enabledServers = serversCache.filter(function(s) { return s.enabled; }); if (!enabledServers.length) { currentServerId = null; currentServer = null; } else { var desired = currentServerId; var selected = desired ? enabledServers.find(function(s) { return s.id === desired; }) : null; if (!selected) { var def = enabledServers.find(function(s) { return s.isDefault; }); selected = def || enabledServers[0]; } currentServer = selected; currentServerId = selected ? selected.id : null; } renderServerSelector(); renderServerSubtitle(); updateRestartBanner(); }) .catch(function(err) { console.error('Error loading servers:', err); serversCache = []; currentServerId = null; currentServer = null; renderServerSelector(); renderServerSubtitle(); updateRestartBanner(); }); } function renderServerSelector() { var container = document.getElementById('serverSelectorContainer'); if (!container) return; var enabledServers = serversCache.filter(function(s) { return s.enabled; }); if (!serversCache.length) { container.innerHTML = '
No servers configured
'; if (typeof updateTranslations === 'function') { updateTranslations(); } return; } if (!enabledServers.length) { container.innerHTML = '
No servers configured
'; if (typeof updateTranslations === 'function') { updateTranslations(); } return; } var options = enabledServers.map(function(server) { var label = escapeHtml(server.name || server.id); var type = server.type ? (' (' + server.type.toUpperCase() + ')') : ''; return ''; }).join(''); container.innerHTML = '' + '
' + ' ' + ' ' + '
'; var select = document.getElementById('serverSelect'); if (select) { select.value = currentServerId || ''; select.addEventListener('change', function(e) { setCurrentServer(e.target.value); }); } if (typeof updateTranslations === 'function') { updateTranslations(); } } function renderServerSubtitle() { var subtitle = document.getElementById('currentServerSubtitle'); if (!subtitle) return; if (!currentServer) { subtitle.textContent = t('servers.selector.none', 'No server configured. Please add a Fail2ban server.'); subtitle.classList.add('text-red-500'); return; } subtitle.classList.remove('text-red-500'); var parts = []; parts.push(currentServer.name || currentServer.id); parts.push(currentServer.type ? currentServer.type.toUpperCase() : 'LOCAL'); if (currentServer.host) { var host = currentServer.host; if (currentServer.port) { host += ':' + currentServer.port; } parts.push(host); } else if (currentServer.hostname) { parts.push(currentServer.hostname); } subtitle.textContent = parts.join(' • '); } function setCurrentServer(serverId) { if (!serverId) { currentServerId = null; currentServer = null; } else { var next = serversCache.find(function(s) { return s.id === serverId && s.enabled; }); currentServer = next || null; currentServerId = currentServer ? currentServer.id : null; } renderServerSelector(); renderServerSubtitle(); updateRestartBanner(); refreshData(); } function openServerManager(serverId) { showLoading(true); loadServers() .then(function() { if (serverId) { editServer(serverId); } else { resetServerForm(); } renderServerManagerList(); openModal('serverManagerModal'); }) .finally(function() { showLoading(false); }); } function renderServerManagerList() { var list = document.getElementById('serverManagerList'); var emptyState = document.getElementById('serverManagerListEmpty'); if (!list || !emptyState) return; if (!serversCache.length) { list.innerHTML = ''; emptyState.classList.remove('hidden'); if (typeof updateTranslations === 'function') updateTranslations(); return; } emptyState.classList.add('hidden'); var html = serversCache.map(function(server) { var statusBadge = server.enabled ? 'Enabled' : 'Disabled'; var defaultBadge = server.isDefault ? 'Default' : ''; var restartBadge = server.restartNeeded ? 'Restart required' : ''; var descriptor = []; if (server.type) { descriptor.push(server.type.toUpperCase()); } if (server.host) { var endpoint = server.host; if (server.port) { endpoint += ':' + server.port; } descriptor.push(endpoint); } else if (server.hostname) { descriptor.push(server.hostname); } var meta = descriptor.join(' • '); var tags = (server.tags || []).length ? '
' + escapeHtml(server.tags.join(', ')) + '
' : ''; return '' + '
' + '
' + '
' + '

' + escapeHtml(server.name || server.id) + defaultBadge + statusBadge + restartBadge + '

' + '

' + escapeHtml(meta || server.id) + '

' + tags + '
' + '
' + ' ' + (server.isDefault ? '' : '') + ' ' + (server.enabled ? (server.type === 'local' ? '' : '') : '') + ' ' + ' ' + '
' + '
' + '
'; }).join(''); list.innerHTML = html; if (typeof updateTranslations === 'function') { updateTranslations(); // Set tooltip text for reload buttons after translations are updated setTimeout(function() { serversCache.forEach(function(server) { if (server.enabled && server.type === 'local') { var buttons = list.querySelectorAll('button[data-i18n="servers.actions.reload"]'); buttons.forEach(function(btn) { var tooltipText = t('servers.actions.reload_tooltip', 'For local connectors, only a configuration reload is possible via the socket connection. The container cannot restart the Fail2ban service using systemctl. To perform a full restart, run \'systemctl restart fail2ban\' directly on the host system.'); btn.setAttribute('title', tooltipText); }); } }); }, 100); } } function resetServerForm() { document.getElementById('serverId').value = ''; document.getElementById('serverName').value = ''; document.getElementById('serverType').value = 'local'; document.getElementById('serverHost').value = ''; document.getElementById('serverPort').value = '22'; document.getElementById('serverSocket').value = '/var/run/fail2ban/fail2ban.sock'; document.getElementById('serverLogPath').value = '/var/log/fail2ban.log'; document.getElementById('serverHostname').value = ''; document.getElementById('serverSSHUser').value = ''; document.getElementById('serverSSHKey').value = ''; document.getElementById('serverAgentUrl').value = ''; document.getElementById('serverAgentSecret').value = ''; document.getElementById('serverTags').value = ''; document.getElementById('serverDefault').checked = false; document.getElementById('serverEnabled').checked = false; populateSSHKeySelect(sshKeysCache || [], ''); onServerTypeChange('local'); } function editServer(serverId) { var server = serversCache.find(function(s) { return s.id === serverId; }); if (!server) return; document.getElementById('serverId').value = server.id || ''; document.getElementById('serverName').value = server.name || ''; document.getElementById('serverType').value = server.type || 'local'; document.getElementById('serverHost').value = server.host || ''; document.getElementById('serverPort').value = server.port || ''; document.getElementById('serverSocket').value = server.socketPath || '/var/run/fail2ban/fail2ban.sock'; document.getElementById('serverLogPath').value = server.logPath || '/var/log/fail2ban.log'; document.getElementById('serverHostname').value = server.hostname || ''; document.getElementById('serverSSHUser').value = server.sshUser || ''; document.getElementById('serverSSHKey').value = server.sshKeyPath || ''; document.getElementById('serverAgentUrl').value = server.agentUrl || ''; document.getElementById('serverAgentSecret').value = server.agentSecret || ''; document.getElementById('serverTags').value = (server.tags || []).join(','); document.getElementById('serverDefault').checked = !!server.isDefault; document.getElementById('serverEnabled').checked = !!server.enabled; onServerTypeChange(server.type || 'local'); if ((server.type || 'local') === 'ssh') { loadSSHKeys().then(function(keys) { populateSSHKeySelect(keys, server.sshKeyPath || ''); }); } } function onServerTypeChange(type) { document.querySelectorAll('[data-server-fields]').forEach(function(el) { var values = (el.getAttribute('data-server-fields') || '').split(/\s+/); if (values.indexOf(type) !== -1) { el.classList.remove('hidden'); } else { el.classList.add('hidden'); } }); var enabledToggle = document.getElementById('serverEnabled'); if (!enabledToggle) return; var isEditing = !!document.getElementById('serverId').value; if (isEditing) { return; } if (type === 'local') { enabledToggle.checked = false; } else { enabledToggle.checked = true; } if (type === 'ssh') { loadSSHKeys().then(function(keys) { if (!isEditing) { populateSSHKeySelect(keys, ''); } }); } else { populateSSHKeySelect([], ''); } } function submitServerForm(event) { event.preventDefault(); showLoading(true); var payload = { id: document.getElementById('serverId').value || undefined, name: document.getElementById('serverName').value.trim(), type: document.getElementById('serverType').value, host: document.getElementById('serverHost').value.trim(), port: document.getElementById('serverPort').value ? parseInt(document.getElementById('serverPort').value, 10) : undefined, socketPath: document.getElementById('serverSocket').value.trim(), logPath: document.getElementById('serverLogPath').value.trim(), hostname: document.getElementById('serverHostname').value.trim(), sshUser: document.getElementById('serverSSHUser').value.trim(), sshKeyPath: document.getElementById('serverSSHKey').value.trim(), agentUrl: document.getElementById('serverAgentUrl').value.trim(), agentSecret: document.getElementById('serverAgentSecret').value.trim(), tags: document.getElementById('serverTags').value ? document.getElementById('serverTags').value.split(',').map(function(tag) { return tag.trim(); }).filter(Boolean) : [], enabled: document.getElementById('serverEnabled').checked }; if (!payload.socketPath) delete payload.socketPath; if (!payload.logPath) delete payload.logPath; if (!payload.hostname) delete payload.hostname; if (!payload.agentUrl) delete payload.agentUrl; if (!payload.agentSecret) delete payload.agentSecret; if (!payload.sshUser) delete payload.sshUser; if (!payload.sshKeyPath) delete payload.sshKeyPath; if (document.getElementById('serverDefault').checked) { payload.isDefault = true; } if (payload.type !== 'local' && payload.type !== 'ssh') { delete payload.socketPath; } if (payload.type !== 'local') { delete payload.logPath; } if (payload.type !== 'ssh') { delete payload.sshUser; delete payload.sshKeyPath; } if (payload.type !== 'agent') { delete payload.agentUrl; delete payload.agentSecret; } fetch('/api/servers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) .then(function(res) { return res.json(); }) .then(function(data) { if (data.error) { showToast('Error saving server: ' + (data.error || 'Unknown error'), 'error'); return; } showToast(t('servers.form.success', 'Server saved successfully.'), 'success'); var saved = data.server || {}; currentServerId = saved.id || currentServerId; return loadServers().then(function() { renderServerManagerList(); renderServerSelector(); renderServerSubtitle(); if (currentServerId) { currentServer = serversCache.find(function(s) { return s.id === currentServerId; }) || currentServer; } return refreshData({ silent: true }); }); }) .catch(function(err) { showToast('Error saving server: ' + err, 'error'); }) .finally(function() { showLoading(false); }); } function populateSSHKeySelect(keys, selected) { var select = document.getElementById('serverSSHKeySelect'); if (!select) return; var options = ''; var selectedInList = false; if (keys && keys.length) { keys.forEach(function(key) { var safe = escapeHtml(key); if (selected && key === selected) { selectedInList = true; } options += ''; }); } else { options += ''; } if (selected && !selectedInList) { var safeSelected = escapeHtml(selected); options += ''; } select.innerHTML = options; if (selected) { select.value = selected; } else { select.value = ''; } if (typeof updateTranslations === 'function') { updateTranslations(); } } function loadSSHKeys() { if (sshKeysCache !== null) { populateSSHKeySelect(sshKeysCache, document.getElementById('serverSSHKey').value); return Promise.resolve(sshKeysCache); } return fetch('/api/ssh/keys') .then(function(res) { return res.json(); }) .then(function(data) { sshKeysCache = data.keys || []; populateSSHKeySelect(sshKeysCache, document.getElementById('serverSSHKey').value); return sshKeysCache; }) .catch(function(err) { console.error('Error loading SSH keys:', err); sshKeysCache = []; populateSSHKeySelect(sshKeysCache, document.getElementById('serverSSHKey').value); return sshKeysCache; }); } function setServerEnabled(serverId, enabled) { var server = serversCache.find(function(s) { return s.id === serverId; }); if (!server) { return; } var payload = Object.assign({}, server, { enabled: enabled }); showLoading(true); fetch('/api/servers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) .then(function(res) { return res.json(); }) .then(function(data) { if (data.error) { showToast('Error saving server: ' + (data.error || 'Unknown error'), 'error'); return; } if (!enabled && currentServerId === serverId) { currentServerId = null; currentServer = null; } return loadServers().then(function() { renderServerManagerList(); renderServerSelector(); renderServerSubtitle(); return refreshData({ silent: true }); }); }) .catch(function(err) { showToast('Error saving server: ' + err, 'error'); }) .finally(function() { showLoading(false); }); } function testServerConnection(serverId) { if (!serverId) return; showLoading(true); fetch('/api/servers/' + encodeURIComponent(serverId) + '/test', { method: 'POST' }) .then(function(res) { return res.json(); }) .then(function(data) { if (data.error) { showToast(t(data.messageKey || 'servers.actions.test_failure', data.error), 'error'); return; } showToast(t(data.messageKey || 'servers.actions.test_success', data.message || 'Connection successful'), 'success'); }) .catch(function(err) { showToast(t('servers.actions.test_failure', 'Connection failed') + ': ' + err, 'error'); }) .finally(function() { showLoading(false); }); } function deleteServer(serverId) { if (!confirm(t('servers.actions.delete_confirm', 'Delete this server entry?'))) return; showLoading(true); fetch('/api/servers/' + encodeURIComponent(serverId), { method: 'DELETE' }) .then(function(res) { return res.json(); }) .then(function(data) { if (data.error) { showToast('Error deleting server: ' + (data.error || 'Unknown error'), 'error'); return; } if (currentServerId === serverId) { currentServerId = null; currentServer = null; } return loadServers().then(function() { renderServerManagerList(); renderServerSelector(); renderServerSubtitle(); return refreshData({ silent: true }); }).then(function() { showToast(t('servers.actions.delete_success', 'Server removed'), 'success'); }); }) .catch(function(err) { showToast('Error deleting server: ' + err, 'error'); }) .finally(function() { showLoading(false); }); } function makeDefaultServer(serverId) { showLoading(true); fetch('/api/servers/' + encodeURIComponent(serverId) + '/default', { method: 'POST' }) .then(function(res) { return res.json(); }) .then(function(data) { if (data.error) { showToast('Error setting default server: ' + (data.error || 'Unknown error'), 'error'); return; } currentServerId = data.server ? data.server.id : serverId; return loadServers().then(function() { renderServerManagerList(); renderServerSelector(); renderServerSubtitle(); return refreshData({ silent: true }); }).then(function() { showToast(t('servers.actions.set_default_success', 'Server set as default'), 'success'); }); }) .catch(function(err) { showToast('Error setting default server: ' + err, 'error'); }) .finally(function() { showLoading(false); }); } function restartFail2banServer(serverId) { if (!serverId) { showToast("No server selected", 'error'); return; } var server = serversCache.find(function(s) { return s.id === serverId; }); var isLocal = server && server.type === 'local'; var confirmMsg = isLocal ? "Reload Fail2ban configuration on this server now? This will reload the configuration without restarting the service." : "Keep in mind that while fail2ban is restarting, logs are not being parsed and no IP addresses are blocked. Restart fail2ban on this server now? This will take some time."; if (!confirm(confirmMsg)) return; showLoading(true); fetch('/api/fail2ban/restart?serverId=' + encodeURIComponent(serverId), { method: 'POST', headers: { 'Content-Type': 'application/json' } }) .then(function(res) { return res.json(); }) .then(function(data) { if (data.error) { showToast("Failed to restart Fail2ban: " + data.error, 'error'); return; } var mode = data.mode || 'restart'; var key, fallback; if (mode === 'reload') { key = 'restart_banner.reload_success'; fallback = 'Fail2ban configuration reloaded successfully'; } else { key = 'restart_banner.restart_success'; fallback = 'Fail2ban service restarted and passed health check'; } return loadServers().then(function() { updateRestartBanner(); showToast(t(key, fallback), 'success'); return refreshData({ silent: true }); }); }) .catch(function(err) { showToast("Failed to restart Fail2ban: " + err, 'error'); }) .finally(function() { showLoading(false); }); } function restartFail2ban() { if (!confirm("Keep in mind that while fail2ban is restarting, logs are not being parsed and no IP addresses are blocked. Restart fail2ban now? This will take some time.")) return; restartFail2banServer(currentServerId); }