2026-02-18 21:48:22 +01:00
// Settings page javascript logics for Fail2ban UI.
2025-12-05 14:30:28 +01:00
"use strict" ;
2026-02-18 21:48:22 +01:00
// =========================================================================
// Load Settings
// =========================================================================
2025-12-16 22:22:32 +01:00
2025-12-05 14:30:28 +01:00
function loadSettings ( ) {
showLoading ( true ) ;
fetch ( '/api/settings' )
. then ( res => res . json ( ) )
. then ( data => {
document . getElementById ( 'languageSelect' ) . value = data . language || 'en' ;
const uiPortInput = document . getElementById ( 'uiPort' ) ;
const portEnvHint = document . getElementById ( 'portEnvHint' ) ;
const portEnvValue = document . getElementById ( 'portEnvValue' ) ;
const portRestartHint = document . getElementById ( 'portRestartHint' ) ;
if ( data . portEnvSet ) {
uiPortInput . value = data . port || data . portFromEnv || 8080 ;
uiPortInput . readOnly = true ;
uiPortInput . classList . add ( 'bg-gray-100' , 'cursor-not-allowed' ) ;
portEnvValue . textContent = data . portFromEnv || data . port || 8080 ;
portEnvHint . style . display = 'block' ;
portRestartHint . style . display = 'none' ;
} else {
uiPortInput . value = data . port || 8080 ;
uiPortInput . readOnly = false ;
uiPortInput . classList . remove ( 'bg-gray-100' , 'cursor-not-allowed' ) ;
portEnvHint . style . display = 'none' ;
portRestartHint . style . display = 'block' ;
}
document . getElementById ( 'debugMode' ) . checked = data . debug || false ;
2026-01-14 21:47:17 +01:00
const consoleOutputEl = document . getElementById ( 'consoleOutput' ) ;
if ( consoleOutputEl ) {
consoleOutputEl . checked = data . consoleOutput || false ;
if ( typeof wasConsoleEnabledOnLoad !== 'undefined' ) {
wasConsoleEnabledOnLoad = consoleOutputEl . checked ;
}
2026-02-18 21:48:22 +01:00
toggleConsoleOutput ( false ) ;
2026-01-14 21:47:17 +01:00
}
2025-12-05 14:30:28 +01:00
const callbackURLInput = document . getElementById ( 'callbackURL' ) ;
callbackURLInput . value = data . callbackUrl || '' ;
2026-02-10 18:32:01 +01:00
const callbackUrlEnvHint = document . getElementById ( 'callbackUrlEnvHint' ) ;
const callbackUrlEnvValue = document . getElementById ( 'callbackUrlEnvValue' ) ;
const callbackUrlDefaultHint = document . getElementById ( 'callbackUrlDefaultHint' ) ;
if ( data . callbackUrlEnvSet ) {
callbackURLInput . value = data . callbackUrlFromEnv || data . callbackUrl || '' ;
callbackURLInput . readOnly = true ;
callbackURLInput . classList . add ( 'bg-gray-100' , 'cursor-not-allowed' ) ;
callbackUrlEnvValue . textContent = data . callbackUrlFromEnv || data . callbackUrl || '' ;
callbackUrlEnvHint . style . display = 'block' ;
callbackUrlDefaultHint . style . display = 'none' ;
} else {
callbackURLInput . readOnly = false ;
callbackURLInput . classList . remove ( 'bg-gray-100' , 'cursor-not-allowed' ) ;
callbackUrlEnvHint . style . display = 'none' ;
callbackUrlDefaultHint . style . display = 'block' ;
}
2025-12-15 23:16:48 +01:00
const callbackSecretInput = document . getElementById ( 'callbackSecret' ) ;
const toggleLink = document . getElementById ( 'toggleCallbackSecretLink' ) ;
if ( callbackSecretInput ) {
callbackSecretInput . value = data . callbackSecret || '' ;
if ( callbackSecretInput . type === 'text' ) {
callbackSecretInput . type = 'password' ;
}
if ( toggleLink ) {
toggleLink . textContent = 'show secret' ;
}
}
2025-12-05 14:30:28 +01:00
2026-02-18 21:48:22 +01:00
// Syncs callback URL when port changes (only when using localhost)
2025-12-05 14:30:28 +01:00
function updateCallbackURLIfDefault ( ) {
2026-02-18 21:48:22 +01:00
if ( data . callbackUrlEnvSet ) return ;
2025-12-05 14:30:28 +01:00
const currentPort = parseInt ( uiPortInput . value , 10 ) || 8080 ;
const currentCallbackURL = callbackURLInput . value . trim ( ) ;
const defaultPattern = /^http:\/\/127\.0\.0\.1:\d+$/ ;
if ( currentCallbackURL === '' || defaultPattern . test ( currentCallbackURL ) ) {
callbackURLInput . value = 'http://127.0.0.1:' + currentPort ;
}
}
uiPortInput . addEventListener ( 'input' , updateCallbackURLIfDefault ) ;
document . getElementById ( 'destEmail' ) . value = data . destemail || '' ;
2025-12-16 22:22:32 +01:00
document . getElementById ( 'emailAlertsForBans' ) . checked = data . emailAlertsForBans !== undefined ? data . emailAlertsForBans : true ;
document . getElementById ( 'emailAlertsForUnbans' ) . checked = data . emailAlertsForUnbans !== undefined ? data . emailAlertsForUnbans : false ;
updateEmailFieldsState ( ) ;
2025-12-05 14:30:28 +01:00
const select = document . getElementById ( 'alertCountries' ) ;
for ( let i = 0 ; i < select . options . length ; i ++ ) {
select . options [ i ] . selected = false ;
}
if ( ! data . alertCountries || data . alertCountries . length === 0 ) {
select . options [ 0 ] . selected = true ;
} else {
for ( let i = 0 ; i < select . options . length ; i ++ ) {
let val = select . options [ i ] . value ;
if ( data . alertCountries . includes ( val ) ) {
select . options [ i ] . selected = true ;
}
}
}
$ ( '#alertCountries' ) . trigger ( 'change' ) ;
checkAndApplyLOTRTheme ( data . alertCountries || [ ] ) ;
if ( data . smtp ) {
document . getElementById ( 'smtpHost' ) . value = data . smtp . host || '' ;
document . getElementById ( 'smtpPort' ) . value = data . smtp . port || 587 ;
document . getElementById ( 'smtpUsername' ) . value = data . smtp . username || '' ;
document . getElementById ( 'smtpPassword' ) . value = data . smtp . password || '' ;
document . getElementById ( 'smtpFrom' ) . value = data . smtp . from || '' ;
2026-01-22 19:34:05 +01:00
document . getElementById ( 'smtpUseTLS' ) . checked = data . smtp . useTLS !== undefined ? data . smtp . useTLS : true ;
document . getElementById ( 'smtpInsecureSkipVerify' ) . checked = data . smtp . insecureSkipVerify || false ;
document . getElementById ( 'smtpAuthMethod' ) . value = data . smtp . authMethod || 'auto' ;
2025-12-05 14:30:28 +01:00
}
document . getElementById ( 'bantimeIncrement' ) . checked = data . bantimeIncrement || false ;
2025-12-15 18:57:50 +01:00
document . getElementById ( 'defaultJailEnable' ) . checked = data . defaultJailEnable || false ;
2025-12-15 21:50:19 +01:00
const geoipProvider = data . geoipProvider || 'builtin' ;
document . getElementById ( 'geoipProvider' ) . value = geoipProvider ;
onGeoIPProviderChange ( geoipProvider ) ;
document . getElementById ( 'geoipDatabasePath' ) . value = data . geoipDatabasePath || '/usr/share/GeoIP/GeoLite2-Country.mmdb' ;
document . getElementById ( 'maxLogLines' ) . value = data . maxLogLines || 50 ;
2025-12-05 14:30:28 +01:00
document . getElementById ( 'banTime' ) . value = data . bantime || '' ;
2026-02-08 19:43:34 +01:00
document . getElementById ( 'bantimeRndtime' ) . value = data . bantimeRndtime || '' ;
2025-12-05 14:30:28 +01:00
document . getElementById ( 'findTime' ) . value = data . findtime || '' ;
document . getElementById ( 'maxRetry' ) . value = data . maxretry || '' ;
2026-02-08 19:43:34 +01:00
document . getElementById ( 'defaultChain' ) . value = data . chain || 'INPUT' ;
2025-12-05 14:30:28 +01:00
const ignoreIPs = data . ignoreips || [ ] ;
renderIgnoreIPsTags ( ignoreIPs ) ;
2026-01-21 19:23:42 +01:00
document . getElementById ( 'banaction' ) . value = data . banaction || 'nftables-multiport' ;
document . getElementById ( 'banactionAllports' ) . value = data . banactionAllports || 'nftables-allports' ;
2025-12-05 14:30:28 +01:00
applyAdvancedActionsSettings ( data . advancedActions || { } ) ;
loadPermanentBlockLog ( ) ;
} )
. catch ( err => {
showToast ( 'Error loading settings: ' + err , 'error' ) ;
} )
. finally ( ( ) => showLoading ( false ) ) ;
}
2026-02-18 21:48:22 +01:00
// =========================================================================
// Save Settings
// =========================================================================
2025-12-05 14:30:28 +01:00
function saveSettings ( event ) {
event . preventDefault ( ) ;
if ( ! validateAllSettings ( ) ) {
showToast ( 'Please fix validation errors before saving' , 'error' ) ;
return ;
}
showLoading ( true ) ;
2026-01-22 19:34:05 +01:00
const smtpPort = parseInt ( document . getElementById ( 'smtpPort' ) . value , 10 ) ;
if ( isNaN ( smtpPort ) || smtpPort < 1 || smtpPort > 65535 ) {
showToast ( 'SMTP port must be between 1 and 65535' , 'error' ) ;
showLoading ( false ) ;
return ;
}
2025-12-05 14:30:28 +01:00
const smtpSettings = {
host : document . getElementById ( 'smtpHost' ) . value . trim ( ) ,
2026-01-22 19:34:05 +01:00
port : smtpPort ,
2025-12-05 14:30:28 +01:00
username : document . getElementById ( 'smtpUsername' ) . value . trim ( ) ,
password : document . getElementById ( 'smtpPassword' ) . value . trim ( ) ,
from : document . getElementById ( 'smtpFrom' ) . value . trim ( ) ,
useTLS : document . getElementById ( 'smtpUseTLS' ) . checked ,
2026-01-22 19:34:05 +01:00
insecureSkipVerify : document . getElementById ( 'smtpInsecureSkipVerify' ) . checked ,
authMethod : document . getElementById ( 'smtpAuthMethod' ) . value || 'auto' ,
2025-12-05 14:30:28 +01:00
} ;
const selectedCountries = Array . from ( document . getElementById ( 'alertCountries' ) . selectedOptions ) . map ( opt => opt . value ) ;
const callbackURLInput = document . getElementById ( 'callbackURL' ) ;
let callbackUrl = callbackURLInput . value . trim ( ) ;
const currentPort = parseInt ( document . getElementById ( 'uiPort' ) . value , 10 ) || 8080 ;
const defaultPattern = /^http:\/\/127\.0\.0\.1:\d+$/ ;
if ( callbackUrl === '' || defaultPattern . test ( callbackUrl ) ) {
callbackUrl = 'http://127.0.0.1:' + currentPort ;
}
const settingsData = {
language : document . getElementById ( 'languageSelect' ) . value ,
port : currentPort ,
debug : document . getElementById ( 'debugMode' ) . checked ,
2026-01-14 21:47:17 +01:00
consoleOutput : document . getElementById ( 'consoleOutput' ) ? document . getElementById ( 'consoleOutput' ) . checked : false ,
2025-12-05 14:30:28 +01:00
destemail : document . getElementById ( 'destEmail' ) . value . trim ( ) ,
callbackUrl : callbackUrl ,
2025-12-15 23:16:48 +01:00
callbackSecret : document . getElementById ( 'callbackSecret' ) . value . trim ( ) ,
2025-12-05 14:30:28 +01:00
alertCountries : selectedCountries . length > 0 ? selectedCountries : [ "ALL" ] ,
2025-12-16 22:22:32 +01:00
emailAlertsForBans : document . getElementById ( 'emailAlertsForBans' ) . checked ,
emailAlertsForUnbans : document . getElementById ( 'emailAlertsForUnbans' ) . checked ,
2025-12-05 14:30:28 +01:00
bantimeIncrement : document . getElementById ( 'bantimeIncrement' ) . checked ,
2025-12-15 18:57:50 +01:00
defaultJailEnable : document . getElementById ( 'defaultJailEnable' ) . checked ,
2025-12-05 14:30:28 +01:00
bantime : document . getElementById ( 'banTime' ) . value . trim ( ) ,
2026-02-08 19:43:34 +01:00
bantimeRndtime : document . getElementById ( 'bantimeRndtime' ) . value . trim ( ) ,
2025-12-05 14:30:28 +01:00
findtime : document . getElementById ( 'findTime' ) . value . trim ( ) ,
maxretry : parseInt ( document . getElementById ( 'maxRetry' ) . value , 10 ) || 3 ,
ignoreips : getIgnoreIPsArray ( ) ,
banaction : document . getElementById ( 'banaction' ) . value ,
banactionAllports : document . getElementById ( 'banactionAllports' ) . value ,
2026-02-08 19:43:34 +01:00
chain : document . getElementById ( 'defaultChain' ) . value || 'INPUT' ,
2025-12-15 21:50:19 +01:00
geoipProvider : document . getElementById ( 'geoipProvider' ) . value || 'builtin' ,
geoipDatabasePath : document . getElementById ( 'geoipDatabasePath' ) . value || '/usr/share/GeoIP/GeoLite2-Country.mmdb' ,
maxLogLines : parseInt ( document . getElementById ( 'maxLogLines' ) . value , 10 ) || 50 ,
2025-12-05 14:30:28 +01:00
smtp : smtpSettings ,
advancedActions : collectAdvancedActionsSettings ( )
} ;
fetch ( '/api/settings' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( settingsData ) ,
} )
. then ( res => res . json ( ) )
. then ( data => {
if ( data . error ) {
showToast ( 'Error saving settings: ' + ( data . error + ( data . details || '' ) ) , 'error' ) ;
} else {
var selectedLang = $ ( '#languageSelect' ) . val ( ) ;
loadTranslations ( selectedLang ) ;
console . log ( "Settings saved successfully. Restart needed? " + ( data . restartNeeded || false ) ) ;
const selectedCountries = Array . from ( document . getElementById ( 'alertCountries' ) . selectedOptions ) . map ( opt => opt . value ) ;
checkAndApplyLOTRTheme ( selectedCountries . length > 0 ? selectedCountries : [ "ALL" ] ) ;
if ( data . restartNeeded ) {
showToast ( t ( 'settings.save_success' , 'Settings saved. Fail2ban restart required.' ) , 'info' ) ;
loadServers ( ) . then ( function ( ) {
updateRestartBanner ( ) ;
} ) ;
} else {
showToast ( t ( 'settings.save_success' , 'Settings saved and fail2ban reloaded' ) , 'success' ) ;
}
}
} )
. catch ( err => showToast ( 'Error saving settings: ' + err , 'error' ) )
. finally ( ( ) => showLoading ( false ) ) ;
}
2026-02-18 21:48:22 +01:00
// =========================================================================
// Email Settings
// =========================================================================
function updateEmailFieldsState ( ) {
const emailAlertsForBans = document . getElementById ( 'emailAlertsForBans' ) . checked ;
const emailAlertsForUnbans = document . getElementById ( 'emailAlertsForUnbans' ) . checked ;
const emailEnabled = emailAlertsForBans || emailAlertsForUnbans ;
const emailFields = [
document . getElementById ( 'destEmail' ) ,
document . getElementById ( 'smtpHost' ) ,
document . getElementById ( 'smtpPort' ) ,
document . getElementById ( 'smtpUsername' ) ,
document . getElementById ( 'smtpPassword' ) ,
document . getElementById ( 'smtpFrom' ) ,
document . getElementById ( 'smtpAuthMethod' ) ,
document . getElementById ( 'smtpUseTLS' ) ,
document . getElementById ( 'smtpInsecureSkipVerify' ) ,
document . getElementById ( 'sendTestEmailBtn' )
] ;
emailFields . forEach ( field => {
if ( field ) {
field . disabled = ! emailEnabled ;
}
} ) ;
}
2025-12-05 14:30:28 +01:00
function sendTestEmail ( ) {
showLoading ( true ) ;
fetch ( '/api/settings/test-email' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' }
} )
. then ( res => res . json ( ) )
. then ( data => {
if ( data . error ) {
showToast ( 'Error sending test email: ' + data . error , 'error' ) ;
} else {
showToast ( 'Test email sent successfully!' , 'success' ) ;
}
} )
. catch ( error => showToast ( 'Error sending test email: ' + error , 'error' ) )
. finally ( ( ) => showLoading ( false ) ) ;
}
2026-02-18 21:48:22 +01:00
// =========================================================================
// Advanced Actions
// =========================================================================
2025-12-05 14:30:28 +01:00
function applyAdvancedActionsSettings ( cfg ) {
cfg = cfg || { } ;
2026-01-14 17:36:06 +01:00
const enabledEl = document . getElementById ( 'advancedActionsEnabled' ) ;
if ( enabledEl ) enabledEl . checked = ! ! cfg . enabled ;
const thresholdEl = document . getElementById ( 'advancedThreshold' ) ;
if ( thresholdEl ) thresholdEl . value = cfg . threshold || 5 ;
2025-12-05 14:30:28 +01:00
const integrationSelect = document . getElementById ( 'advancedIntegrationSelect' ) ;
2026-01-14 17:36:06 +01:00
if ( integrationSelect ) integrationSelect . value = cfg . integration || '' ;
2025-12-05 14:30:28 +01:00
const mk = cfg . mikrotik || { } ;
2026-01-14 17:36:06 +01:00
const mkHost = document . getElementById ( 'mikrotikHost' ) ;
if ( mkHost ) mkHost . value = mk . host || '' ;
const mkPort = document . getElementById ( 'mikrotikPort' ) ;
if ( mkPort ) mkPort . value = mk . port || 22 ;
const mkUser = document . getElementById ( 'mikrotikUsername' ) ;
if ( mkUser ) mkUser . value = mk . username || '' ;
const mkPass = document . getElementById ( 'mikrotikPassword' ) ;
if ( mkPass ) mkPass . value = mk . password || '' ;
const mkKey = document . getElementById ( 'mikrotikSSHKey' ) ;
if ( mkKey ) mkKey . value = mk . sshKeyPath || '' ;
const mkList = document . getElementById ( 'mikrotikList' ) ;
if ( mkList ) mkList . value = mk . addressList || 'fail2ban-permanent' ;
2025-12-05 14:30:28 +01:00
const pf = cfg . pfSense || { } ;
2026-01-14 17:36:06 +01:00
const pfURL = document . getElementById ( 'pfSenseBaseURL' ) ;
if ( pfURL ) pfURL . value = pf . baseUrl || '' ;
const pfToken = document . getElementById ( 'pfSenseToken' ) ;
if ( pfToken ) pfToken . value = pf . apiToken || '' ;
const pfAlias = document . getElementById ( 'pfSenseAlias' ) ;
if ( pfAlias ) pfAlias . value = pf . alias || '' ;
const pfTLS = document . getElementById ( 'pfSenseSkipTLS' ) ;
if ( pfTLS ) pfTLS . checked = ! ! pf . skipTLSVerify ;
const opn = cfg . opnsense || { } ;
const opnURL = document . getElementById ( 'opnsenseBaseURL' ) ;
if ( opnURL ) opnURL . value = opn . baseUrl || '' ;
const opnKey = document . getElementById ( 'opnsenseKey' ) ;
if ( opnKey ) opnKey . value = opn . apiKey || '' ;
const opnSecret = document . getElementById ( 'opnsenseSecret' ) ;
if ( opnSecret ) opnSecret . value = opn . apiSecret || '' ;
const opnAlias = document . getElementById ( 'opnsenseAlias' ) ;
if ( opnAlias ) opnAlias . value = opn . alias || '' ;
const opnTLS = document . getElementById ( 'opnsenseSkipTLS' ) ;
if ( opnTLS ) opnTLS . checked = ! ! opn . skipTLSVerify ;
2025-12-05 14:30:28 +01:00
updateAdvancedIntegrationFields ( ) ;
}
function collectAdvancedActionsSettings ( ) {
return {
enabled : document . getElementById ( 'advancedActionsEnabled' ) . checked ,
threshold : parseInt ( document . getElementById ( 'advancedThreshold' ) . value , 10 ) || 5 ,
integration : document . getElementById ( 'advancedIntegrationSelect' ) . value ,
mikrotik : {
host : document . getElementById ( 'mikrotikHost' ) . value . trim ( ) ,
port : parseInt ( document . getElementById ( 'mikrotikPort' ) . value , 10 ) || 22 ,
username : document . getElementById ( 'mikrotikUsername' ) . value . trim ( ) ,
password : document . getElementById ( 'mikrotikPassword' ) . value ,
sshKeyPath : document . getElementById ( 'mikrotikSSHKey' ) . value . trim ( ) ,
addressList : document . getElementById ( 'mikrotikList' ) . value . trim ( ) || 'fail2ban-permanent' ,
} ,
pfSense : {
baseUrl : document . getElementById ( 'pfSenseBaseURL' ) . value . trim ( ) ,
apiToken : document . getElementById ( 'pfSenseToken' ) . value . trim ( ) ,
alias : document . getElementById ( 'pfSenseAlias' ) . value . trim ( ) ,
skipTLSVerify : document . getElementById ( 'pfSenseSkipTLS' ) . checked ,
2026-01-14 17:36:06 +01:00
} ,
opnsense : {
baseUrl : document . getElementById ( 'opnsenseBaseURL' ) . value . trim ( ) ,
apiKey : document . getElementById ( 'opnsenseKey' ) . value . trim ( ) ,
apiSecret : document . getElementById ( 'opnsenseSecret' ) . value . trim ( ) ,
alias : document . getElementById ( 'opnsenseAlias' ) . value . trim ( ) ,
skipTLSVerify : document . getElementById ( 'opnsenseSkipTLS' ) . checked ,
2025-12-05 14:30:28 +01:00
}
} ;
}
function updateAdvancedIntegrationFields ( ) {
const selected = document . getElementById ( 'advancedIntegrationSelect' ) . value ;
document . getElementById ( 'advancedMikrotikFields' ) . classList . toggle ( 'hidden' , selected !== 'mikrotik' ) ;
document . getElementById ( 'advancedPfSenseFields' ) . classList . toggle ( 'hidden' , selected !== 'pfsense' ) ;
2026-01-14 17:36:06 +01:00
document . getElementById ( 'advancedOPNsenseFields' ) . classList . toggle ( 'hidden' , selected !== 'opnsense' ) ;
2025-12-05 14:30:28 +01:00
}
2026-02-18 21:48:22 +01:00
// =========================================================================
// Permanent Block Log
// =========================================================================
2025-12-05 14:30:28 +01:00
function loadPermanentBlockLog ( ) {
fetch ( '/api/advanced-actions/blocks' )
. then ( res => res . json ( ) )
. then ( data => {
if ( data . error ) {
showToast ( 'Error loading permanent block log: ' + data . error , 'error' ) ;
return ;
}
renderPermanentBlockLog ( data . blocks || [ ] ) ;
} )
. catch ( err => {
showToast ( 'Error loading permanent block log: ' + err , 'error' ) ;
} ) ;
}
2026-01-24 15:15:13 +01:00
function renderPermanentBlockLogRow ( block ) {
const statusClass = block . status === 'blocked'
? 'text-green-600'
: ( block . status === 'unblocked' ? 'text-gray-500' : 'text-red-600' ) ;
const message = block . message ? escapeHtml ( block . message ) : '' ;
return ''
+ '<tr class="border-t">'
+ ' <td class="px-3 py-2 font-mono text-sm">' + escapeHtml ( block . ip ) + '</td>'
+ ' <td class="px-3 py-2 text-sm">' + escapeHtml ( block . integration ) + '</td>'
+ ' <td class="px-3 py-2 text-sm ' + statusClass + '">' + escapeHtml ( block . status ) + '</td>'
+ ' <td class="px-3 py-2 text-sm">' + ( message || ' ' ) + '</td>'
+ ' <td class="px-3 py-2 text-xs text-gray-500">' + escapeHtml ( block . serverId || '' ) + '</td>'
+ ' <td class="px-3 py-2 text-xs text-gray-500">' + ( block . updatedAt ? new Date ( block . updatedAt ) . toLocaleString ( ) : '' ) + '</td>'
+ ' <td class="px-3 py-2 text-right">'
+ ' <button type="button" class="text-sm text-blue-600 hover:text-blue-800" onclick="advancedUnblockIP(\'' + escapeHtml ( block . ip ) + '\', event)" data-i18n="settings.advanced.unblock_btn">Remove</button>'
+ ' </td>'
+ '</tr>' ;
}
2025-12-05 14:30:28 +01:00
function renderPermanentBlockLog ( blocks ) {
const container = document . getElementById ( 'permanentBlockLog' ) ;
if ( ! container ) return ;
if ( ! blocks . length ) {
container . innerHTML = '<p class="text-sm text-gray-500 p-4" data-i18n="settings.advanced.log_empty">No permanent blocks recorded yet.</p>' ;
if ( typeof updateTranslations === 'function' ) updateTranslations ( ) ;
return ;
}
2026-01-24 15:15:13 +01:00
const maxVisible = 10 ;
const visible = blocks . slice ( 0 , maxVisible ) ;
const hidden = blocks . slice ( maxVisible ) ;
let visibleRows = visible . map ( renderPermanentBlockLogRow ) . join ( '' ) ;
let hiddenRows = hidden . map ( renderPermanentBlockLogRow ) . join ( '' ) ;
let html = ''
2025-12-05 14:30:28 +01:00
+ '<table class="min-w-full text-sm">'
+ ' <thead class="bg-gray-50 text-left">'
+ ' <tr>'
+ ' <th class="px-3 py-2" data-i18n="settings.advanced.log_ip">IP</th>'
+ ' <th class="px-3 py-2" data-i18n="settings.advanced.log_integration">Integration</th>'
+ ' <th class="px-3 py-2" data-i18n="settings.advanced.log_status">Status</th>'
+ ' <th class="px-3 py-2" data-i18n="settings.advanced.log_message">Message</th>'
+ ' <th class="px-3 py-2" data-i18n="settings.advanced.log_server">Server</th>'
+ ' <th class="px-3 py-2" data-i18n="settings.advanced.log_updated">Updated</th>'
+ ' <th class="px-3 py-2 text-right" data-i18n="settings.advanced.log_actions">Actions</th>'
+ ' </tr>'
+ ' </thead>'
2026-01-24 15:15:13 +01:00
+ ' <tbody>' + visibleRows + '</tbody>' ;
if ( hidden . length > 0 ) {
const hiddenId = 'permanentBlockLog-hidden' ;
const toggleId = 'permanentBlockLog-toggle' ;
const moreLabel = ( typeof t === 'function' ? t ( 'dashboard.banned.show_more' , 'Show more' ) : 'Show more' ) + ' +' + hidden . length ;
const lessLabel = typeof t === 'function' ? t ( 'dashboard.banned.show_less' , 'Hide extra' ) : 'Hide extra' ;
html += ''
+ ' <tbody id="' + hiddenId + '" class="hidden" data-initially-hidden="true">' + hiddenRows + '</tbody>'
+ '</table>'
+ '<button type="button" class="text-xs font-semibold text-blue-600 hover:text-blue-800 px-3 py-3 permanent-block-log-toggle"'
+ ' id="' + toggleId + '"'
+ ' data-target="' + hiddenId + '"'
+ ' data-more-label="' + escapeHtml ( moreLabel ) + '"'
+ ' data-less-label="' + escapeHtml ( lessLabel ) + '"'
+ ' data-expanded="false"'
+ ' onclick="toggleBannedList(\'' + hiddenId + '\', \'' + toggleId + '\')">'
+ escapeHtml ( moreLabel )
+ '</button>' ;
} else {
html += '</table>' ;
}
container . innerHTML = html ;
2025-12-05 14:30:28 +01:00
if ( typeof updateTranslations === 'function' ) updateTranslations ( ) ;
}
function refreshPermanentBlockLog ( ) {
loadPermanentBlockLog ( ) ;
}
2026-02-21 17:23:33 +01:00
function clearPermanentBlockLog ( ) {
var msg = t ( 'settings.advanced.clear_log_confirm' ,
'This will permanently delete the entire block log. Fail2ban UI will assume that no IPs are currently blocked on the external firewall.\n\nThis action cannot be undone. Continue?' ) ;
if ( ! confirm ( msg ) ) return ;
fetch ( '/api/advanced-actions/blocks' , { method : 'DELETE' , headers : serverHeaders ( ) } )
. then ( function ( res ) { return res . json ( ) ; } )
. then ( function ( data ) {
if ( data . error ) {
showToast ( data . error , 'error' ) ;
return ;
}
showToast ( t ( 'settings.advanced.clear_log_success' , 'Permanent block log cleared.' ) , 'success' ) ;
loadPermanentBlockLog ( ) ;
} )
. catch ( function ( err ) { showToast ( String ( err ) , 'error' ) ; } ) ;
}
2026-02-18 21:48:22 +01:00
// =========================================================================
// Advanced Test
// =========================================================================
2025-12-05 14:30:28 +01:00
function openAdvancedTestModal ( ) {
document . getElementById ( 'advancedTestIP' ) . value = '' ;
openModal ( 'advancedTestModal' ) ;
}
function submitAdvancedTest ( action ) {
const ipValue = document . getElementById ( 'advancedTestIP' ) . value . trim ( ) ;
if ( ! ipValue ) {
showToast ( 'Please enter an IP address.' , 'info' ) ;
return ;
}
showLoading ( true ) ;
fetch ( '/api/advanced-actions/test' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
2026-01-14 17:36:06 +01:00
body : JSON . stringify ( { action : action , ip : ipValue } )
2025-12-05 14:30:28 +01:00
} )
. then ( res => res . json ( ) )
. then ( data => {
if ( data . error ) {
showToast ( 'Advanced action failed: ' + data . error , 'error' ) ;
} else {
2026-02-18 21:48:22 +01:00
showToast ( data . message || 'Action completed' , data . info ? 'info' : 'success' ) ;
2025-12-05 14:30:28 +01:00
loadPermanentBlockLog ( ) ;
}
} )
. catch ( err => showToast ( 'Advanced action failed: ' + err , 'error' ) )
. finally ( ( ) => {
showLoading ( false ) ;
closeModal ( 'advancedTestModal' ) ;
} ) ;
}
function advancedUnblockIP ( ip , event ) {
if ( event ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
}
if ( ! ip ) return ;
fetch ( '/api/advanced-actions/test' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { action : 'unblock' , ip : ip } )
} )
. then ( res => res . json ( ) )
. then ( data => {
if ( data . error ) {
showToast ( 'Failed to remove IP: ' + data . error , 'error' ) ;
} else {
showToast ( data . message || 'IP removed' , 'success' ) ;
loadPermanentBlockLog ( ) ;
}
} )
. catch ( err => showToast ( 'Failed to remove IP: ' + err , 'error' ) ) ;
}
2026-02-18 21:48:22 +01:00
// =========================================================================
// Misc
// =========================================================================
2025-12-05 14:30:28 +01:00
const advancedIntegrationSelect = document . getElementById ( 'advancedIntegrationSelect' ) ;
if ( advancedIntegrationSelect ) {
advancedIntegrationSelect . addEventListener ( 'change' , updateAdvancedIntegrationFields ) ;
}
2025-12-15 23:16:48 +01:00
function toggleCallbackSecretVisibility ( ) {
const input = document . getElementById ( 'callbackSecret' ) ;
const link = document . getElementById ( 'toggleCallbackSecretLink' ) ;
if ( ! input || ! link ) return ;
const isPassword = input . type === 'password' ;
input . type = isPassword ? 'text' : 'password' ;
link . textContent = isPassword ? 'hide secret' : 'show secret' ;
}
2026-02-18 21:48:22 +01:00
function onGeoIPProviderChange ( provider ) {
const dbPathContainer = document . getElementById ( 'geoipDatabasePathContainer' ) ;
if ( dbPathContainer ) {
if ( provider === 'maxmind' ) {
dbPathContainer . style . display = 'block' ;
} else {
dbPathContainer . style . display = 'none' ;
}
}
}