Files
fail2ban-ui/pkg/web/templates/index.html

1491 lines
106 KiB
HTML
Raw Normal View History

<!--
Fail2ban UI - A Swiss made, management interface for Fail2ban.
Copyright (C) 2025 Swissmakers GmbH
Licensed under the GNU General Public License, Version 3 (GPL-3.0)
You may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.gnu.org/licenses/gpl-3.0.en.html
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
2025-01-25 16:21:14 +01:00
<!DOCTYPE html>
<html lang="en">
2025-01-25 16:21:14 +01:00
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title data-i18n="page.title">Fail2ban UI Dashboard</title>
<!-- Prism.js for syntax highlighting -->
<link rel="stylesheet" href="/static/vendor/prism/prism-tomorrow.min.css?v={{.version}}" />
<script src="/static/vendor/prism/prism-core.min.js?v={{.version}}"></script>
<script src="/static/vendor/prism/prism-autoloader.min.js?v={{.version}}"></script>
<!-- Tailwind CSS -->
<link rel="stylesheet" href="/static/tailwind.css?v={{.version}}">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="/static/vendor/fontawesome/all.min.css?v={{.version}}">
<!-- Select2 CSS -->
<link rel="stylesheet" href="/static/vendor/select2/select2.min.css?v={{.version}}" />
<!-- Fail2ban UI CSS -->
<link rel="stylesheet" href="/static/fail2ban-ui.css?v={{.version}}">
2025-12-01 23:25:54 +01:00
<!-- LOTR Theme CSS (loaded conditionally) -->
<link rel="stylesheet" href="/static/lotr.css?v={{.version}}" id="lotr-css" disabled>
2025-12-01 23:25:54 +01:00
<!-- Google Fonts for LOTR theme -->
<link rel="stylesheet" href="/static/vendor/fonts/google-fonts.css?v={{.version}}">
2025-01-25 16:21:14 +01:00
</head>
<body class="bg-gray-50 overflow-y-scroll" data-skip-login-page="{{if .skipLoginPage}}true{{else}}false{{end}}" data-oidc-enabled="{{if .oidcEnabled}}true{{else}}false{{end}}">
2025-07-19 11:19:40 +02:00
<!-- Loading Overlay -->
<div id="loading-overlay" class="fixed inset-0 flex items-center justify-center z-50 bg-black bg-opacity-50 backdrop-blur-sm">
2025-07-19 11:19:40 +02:00
<div class="h-12 w-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
</div>
2025-07-19 11:19:40 +02:00
<!-- Restart Banner -->
<div id="restartBanner" class="bg-yellow-400 text-gray-900 p-3 text-center">
<div class="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-center gap-4">
<strong data-i18n="restart_banner.message">Fail2ban configuration changed! To apply the changes, please: </strong>
<button class="bg-gray-800 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors" onclick="restartFail2ban()" data-i18n="restart_banner.button">Restart Service</button>
</div>
</div>
<div id="toast-container"></div>
2025-07-19 11:19:40 +02:00
<!-- ******************************************************************* -->
<!-- Navigation START -->
<!-- ******************************************************************* -->
<nav class="hidden bg-blue-600 text-white shadow-lg">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex items-center">
<div class="flex-shrink-0">
<a href="/" class="text-xl font-bold hover:text-blue-200 transition-colors cursor-pointer">Fail2ban UI</a>
</div>
<div id="backendStatus" class="ml-4 flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-gray-400" id="statusDot"></span>
<span id="statusText" class="text-xs">Connecting...</span>
</div>
</div>
<div class="hidden md:block">
<div class="ml-10 flex items-baseline space-x-4 items-center">
<a href="#" onclick="showSection('dashboardSection')" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-blue-700 transition-colors" data-i18n="nav.dashboard">Dashboard</a>
<a href="#" onclick="showSection('filterSection')" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-blue-700 transition-colors" data-i18n="nav.filter_debug">Filter Debug</a>
<a href="#" onclick="showSection('settingsSection')" class="px-3 py-2 rounded-md text-sm font-medium hover:bg-blue-700 transition-colors" data-i18n="nav.settings">Settings</a>
<div id="clockDisplay" class="ml-4 text-sm font-mono">
<span id="clockTime">--:--:--</span>
</div>
<!-- User info and logout (shown when authenticated) -->
<div id="userInfoContainer" class="hidden ml-4 flex items-center gap-3 border-l border-blue-500 pl-4">
<div class="relative">
<button id="userMenuButton" onclick="toggleUserMenu()" class="flex items-center gap-2 px-3 py-2 rounded text-sm font-medium hover:bg-blue-700 transition-colors focus:outline-none">
<span id="userDisplayName" class="font-medium"></span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<!-- User dropdown menu -->
<div id="userMenuDropdown" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50 border border-gray-200">
<div class="px-4 py-2 border-b border-gray-200">
<div class="text-sm font-medium text-gray-900" id="userMenuDisplayName"></div>
<div class="text-xs text-gray-500" id="userMenuEmail"></div>
</div>
<button onclick="handleLogout()" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors" data-i18n="auth.logout">Logout</button>
</div>
</div>
</div>
</div>
</div>
<div class="md:hidden">
<button type="button" class="inline-flex items-center justify-center p-2 rounded-md text-white hover:text-white hover:bg-blue-700 focus:outline-none" onclick="toggleMobileMenu()">
<svg class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile menu -->
<div id="mobileMenu" class="hidden md:hidden">
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
<a href="#" onclick="showSection('dashboardSection')" class="block px-3 py-2 rounded-md text-base font-medium hover:bg-blue-700 transition-colors" data-i18n="nav.dashboard">Dashboard</a>
<a href="#" onclick="showSection('filterSection')" class="block px-3 py-2 rounded-md text-base font-medium hover:bg-blue-700 transition-colors" data-i18n="nav.filter_debug">Filter Debug</a>
<a href="#" onclick="showSection('settingsSection')" class="block px-3 py-2 rounded-md text-base font-medium hover:bg-blue-700 transition-colors" data-i18n="nav.settings">Settings</a>
<!-- User info and logout in mobile menu (shown when authenticated) -->
<div id="mobileUserInfoContainer" class="hidden border-t border-blue-500 mt-2 pt-2">
<div class="px-3 py-2">
<div class="text-sm font-medium" id="mobileUserDisplayName"></div>
<div class="text-xs text-blue-200" id="mobileUserEmail"></div>
</div>
<button onclick="handleLogout()" class="w-full text-left block px-3 py-2 rounded-md text-base font-medium hover:bg-blue-700 transition-colors" data-i18n="auth.logout">Logout</button>
</div>
2025-01-25 21:43:50 +01:00
</div>
</div>
</nav>
2025-07-19 11:19:40 +02:00
<!-- ************************ Navigation END *************************** -->
<!-- Login Page (hidden by default, shown only when OIDC enabled and not authenticated) -->
<div id="loginPage" class="hidden min-h-screen flex items-center justify-center bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full">
<!-- Login Card -->
<div class="bg-white rounded-lg shadow-lg p-8 border border-gray-200">
<!-- Logo and Title -->
<div class="text-center mb-8">
<div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-blue-600 mb-4">
<svg class="h-10 w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
</div>
<h2 class="text-3xl font-bold text-gray-900 mb-2" data-i18n="auth.login_title">Sign in to Fail2ban UI</h2>
<p class="text-sm text-gray-600" data-i18n="auth.login_description">Please authenticate to access the management interface</p>
</div>
<!-- Error Message -->
<div id="loginError" class="hidden bg-red-50 border-l-4 border-red-400 text-red-700 px-4 py-3 rounded mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium" id="loginErrorText"></p>
</div>
</div>
</div>
<!-- Login Button -->
<div class="mb-6">
<button type="button" onclick="handleLogin()" class="w-full flex justify-center items-center py-3 px-4 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
</svg>
<span data-i18n="auth.login_button">Sign in with OIDC</span>
</button>
<!-- Loading State -->
<div id="loginLoading" class="hidden text-center py-4">
<div class="inline-flex items-center">
<div class="h-5 w-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mr-3"></div>
<p class="text-sm text-gray-600 font-medium" data-i18n="auth.logging_in">Redirecting to login...</p>
</div>
</div>
</div>
<!-- Footer Info -->
<div class="pt-6 border-t border-gray-200">
<p class="text-xs text-center text-gray-500">
Secure authentication via OpenID Connect
</p>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<main id="mainContent" class="hidden max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
2025-07-19 11:19:40 +02:00
<!-- ******************************************************************* -->
<!-- Dashboard Page START -->
<!-- ******************************************************************* -->
<div id="dashboardSection">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-800" data-i18n="dashboard.title">Dashboard</h1>
<p id="currentServerSubtitle" class="text-sm text-gray-500 mt-1"></p>
</div>
<div class="flex flex-col sm:flex-row sm:items-center gap-3">
<div id="serverSelectorContainer" class="min-w-[220px]"></div>
{{if not .disableExternalIP}}
<div class="text-sm text-gray-500 sm:ml-2">
<span data-i18n="dashboard.external_ip">Your ext. IP:</span>
<span id="external-ip" class="font-medium text-blue-600 hover:underline cursor-pointer">Loading…</span>
</div>
{{end}}
<div class="flex flex-col sm:flex-row gap-3">
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors flex items-center gap-2"
onclick="openServerManager()">
<i class="fas fa-network-wired"></i>
<span data-i18n="dashboard.manage_servers">Manage Servers</span>
</button>
<button class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors flex items-center gap-2"
onclick="openManageJailsModal()">
<i class="fas fa-cog"></i>
<span data-i18n="dashboard.manage_jails">Manage Jails</span>
</button>
</div>
</div>
</div>
<div id="dashboard"></div> <!-- Dynamic content from the API -->
</div>
2025-07-19 11:19:40 +02:00
<!-- ********************** Dashboard Page END ************************* -->
2025-07-19 11:19:40 +02:00
<!-- ******************************************************************* -->
<!-- Filter-Debug Page START -->
<!-- ******************************************************************* -->
<div id="filterSection" class="hidden">
<h2 class="text-2xl font-bold text-gray-800 mb-6" data-i18n="filter_debug.title">Filter Debug</h2>
2025-07-19 11:19:40 +02:00
<div id="filterNotice" class="hidden mb-4 text-sm text-yellow-700 bg-yellow-100 border border-yellow-200 rounded px-4 py-3"></div>
<div class="bg-white rounded-lg shadow p-6 mb-6">
<div class="mb-4 flex justify-between items-end">
<div class="flex-1 mr-4">
<label for="filterSelect" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="filter_debug.select_filter">Select a Filter</label>
<div class="flex gap-2">
<select id="filterSelect" class="flex-1 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"></select>
<button type="button" onclick="deleteFilter()" id="deleteFilterBtn" class="px-3 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" disabled title="Delete selected filter">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
2025-07-19 11:19:40 +02:00
<!-- Textarea for filter content (readonly by default, editable with Edit button) -->
<div class="mb-4">
<div class="flex items-center justify-between mb-2">
<label for="filterContentTextarea" class="block text-sm font-medium text-gray-700" data-i18n="filter_debug.filter_content">Filter Content</label>
<button type="button" id="editFilterContentBtn" onclick="toggleFilterContentEdit()" class="text-sm px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 hidden" data-i18n="filter_debug.edit_filter">Edit</button>
</div>
<p class="text-xs text-gray-500 mb-2" data-i18n="filter_debug.filter_content_hint_readonly">Filter content is shown read-only. Click 'Edit' to modify for testing. Changes are temporary and not saved.</p>
<p class="text-xs text-gray-500 mb-2 hidden" id="filterContentHintEditable" data-i18n="filter_debug.filter_content_hint">Edit the filter regex below for testing. Changes are temporary and not saved.</p>
<textarea id="filterContentTextarea" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 h-40 font-mono text-sm bg-gray-50"
placeholder="Filter content will appear here when a filter is selected..." readonly></textarea>
</div>
<!-- Textarea for log lines to test -->
<div class="mb-4">
<label for="logLinesTextarea" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="filter_debug.log_lines">Log Lines</label>
<textarea id="logLinesTextarea" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 h-40"
data-i18n-placeholder="filter_debug.log_lines_placeholder" placeholder="Enter log lines here..."></textarea>
</div>
2025-07-19 11:19:40 +02:00
<button class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors" onclick="testSelectedFilter()" data-i18n="filter_debug.test_filter">Test Filter</button>
</div>
2025-07-19 11:19:40 +02:00
<div id="testResults" class="hidden bg-gray-900 rounded-lg shadow p-6 text-white font-mono text-sm"></div>
</div>
2025-07-19 11:19:40 +02:00
<!-- ********************* Filter-Debug Page END *********************** -->
2025-07-19 11:19:40 +02:00
<!-- ******************************************************************* -->
<!-- Settings Page START -->
<!-- ******************************************************************* -->
<div id="settingsSection" class="hidden">
<h2 class="text-2xl font-bold text-gray-800 mb-6" data-i18n="settings.title">Settings</h2>
2025-07-19 11:19:40 +02:00
<form onsubmit="saveSettings(event)" class="space-y-6">
<!-- General Settings Group -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4" data-i18n="settings.general">General Settings</h3>
2025-07-19 11:19:40 +02:00
<!-- Language Selection -->
<div class="mb-4">
<label for="languageSelect" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.language">Language</label>
<select id="languageSelect" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="it">Italiano</option>
<option value="de_ch">Schwiizerdütsch</option>
</select>
</div>
2025-07-19 11:19:40 +02:00
<!-- Fail2Ban UI Port (server) -->
<div class="mb-4">
<label for="uiPort" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.server_port">Server Port</label>
<input type="number" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="uiPort"
data-i18n-placeholder="settings.server_port_placeholder" placeholder="e.g., 8080" required min="80" max="65535" />
<p class="mt-1 text-sm text-gray-500" id="portEnvHint" style="display: none;">
<span data-i18n="settings.port_env_set">Port is set via PORT environment variable:</span>
<span id="portEnvValue"></span>. <span data-i18n="settings.port_env_hint">To change the port via Web UI, remove the PORT environment variable and restart the container.</span>
</p>
<p class="text-xs text-gray-500 mt-1" id="portRestartHint" style="display: none;" data-i18n="settings.port_restart_hint">⚠️ Port changes require a container restart to take effect.</p>
</div>
<div class="mb-4">
<label for="callbackURL" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.callback_url">Fail2ban Callback URL</label>
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="callbackURL"
data-i18n-placeholder="settings.callback_url_placeholder" placeholder="http://127.0.0.1:8080" />
<p class="text-xs text-gray-500 mt-1" data-i18n="settings.callback_url_hint">This URL is used by all Fail2Ban instances to send ban alerts back to Fail2Ban UI. For local deployments, use the same port as Fail2Ban UI (e.g., http://127.0.0.1:8080). For reverse proxy setups, use your TLS-encrypted endpoint (e.g., https://fail2ban.example.com).</p>
</div>
2025-07-19 11:19:40 +02:00
<div class="mb-4">
<div class="flex items-center justify-between mb-2">
<label for="callbackSecret" class="block text-sm font-medium text-gray-700" data-i18n="settings.callback_secret">Fail2ban Callback URL Secret</label>
<a href="#" id="toggleCallbackSecretLink" class="text-sm text-blue-600 hover:text-blue-800 underline" onclick="toggleCallbackSecretVisibility(); return false;">show secret</a>
</div>
<input type="password" class="w-full border border-gray-300 rounded-md px-3 py-2 bg-gray-100 cursor-not-allowed" id="callbackSecret" readonly
data-i18n-placeholder="settings.callback_secret_placeholder" placeholder="Auto-generated 42-character secret" />
<p class="text-xs text-gray-500 mt-1" data-i18n="settings.callback_secret.description">This secret is automatically generated and used to authenticate ban notification requests. It is included in the fail2ban action configuration.</p>
</div>
<!-- Debug Log Output -->
<div class="flex items-center gap-4 border border-gray-200 rounded-lg p-2 overflow-x-auto bg-gray-50">
<div class="flex items-center">
<input type="checkbox" id="debugMode" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out">
<label for="debugMode" class="ml-2 block text-sm text-gray-700" data-i18n="settings.enable_debug">Enable Debug Log</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="consoleOutput" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out" onchange="toggleConsoleOutput(true)">
<label for="consoleOutput" class="ml-2 block text-sm text-gray-700" data-i18n="settings.enable_console">Enable Console Output</label>
</div>
</div>
<!-- Console Output Window -->
<div id="consoleOutputContainer" class="hidden mt-4 border border-gray-700 rounded-lg bg-gray-900 shadow-lg overflow-hidden">
<div class="flex items-center justify-between bg-gray-800 px-4 py-2 border-b border-gray-700">
<div class="flex items-center gap-2">
<div class="flex gap-1.5">
<div class="w-3 h-3 rounded-full bg-red-500"></div>
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
<div class="w-3 h-3 rounded-full bg-green-500"></div>
</div>
<h4 class="text-sm font-medium text-gray-100 ml-2" data-i18n="settings.console.title">Console Output</h4>
</div>
<button type="button" onclick="clearConsole()" class="text-xs text-gray-100 hover:text-white px-2 py-1 rounded hover:bg-gray-700 transition-colors" data-i18n="settings.console.clear">Clear</button>
</div>
<div id="consoleOutputWindow" class="overflow-y-auto p-4 font-mono text-sm text-green-400 bg-gray-900" style="max-height: 430px; min-height: 200px;">
<div class="text-gray-500">Console output will appear here...</div>
</div>
</div>
</div>
<!-- Advanced Actions -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
<div>
<h3 class="text-lg font-medium text-gray-900 mb-2" data-i18n="settings.advanced.title">Advanced Actions for Recurring Offenders</h3>
<p class="text-sm text-gray-500" data-i18n="settings.advanced.description">Automatically add recurring offenders to an external firewall once they hit a specific threshold.</p>
</div>
<div class="flex gap-2">
<button type="button" class="px-3 py-2 text-sm rounded border border-gray-300 text-gray-700 hover:bg-gray-50" onclick="refreshPermanentBlockLog()" data-i18n="settings.advanced.refresh_log">Refresh Log</button>
2025-11-30 13:26:09 +01:00
<button type="button" class="px-3 py-2 text-sm rounded border border-blue-600 text-blue-600 hover:bg-blue-50" onclick="openAdvancedTestModal()" data-i18n="settings.advanced.test_button">Manually Block / Test</button>
</div>
</div>
<div class="mt-4 space-y-4">
<div class="flex items-center">
<input type="checkbox" id="advancedActionsEnabled" class="h-4 w-4 text-blue-600 border-gray-300 rounded">
<label for="advancedActionsEnabled" class="ml-2 text-sm text-gray-700" data-i18n="settings.advanced.enable">Enable automatic permanent blocking</label>
</div>
<div>
<label for="advancedThreshold" class="block text-sm font-medium text-gray-700" data-i18n="settings.advanced.threshold">Threshold before permanent block</label>
<input type="number" id="advancedThreshold" min="1" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="5">
<p class="text-xs text-gray-500 mt-1" data-i18n="settings.advanced.threshold_hint">If an IP is banned at least this many times it will be forwarded to the selected firewall integration.</p>
</div>
<div>
<label for="advancedIntegrationSelect" class="block text-sm font-medium text-gray-700" data-i18n="settings.advanced.integration">Integration</label>
<select id="advancedIntegrationSelect" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="" data-i18n="settings.advanced.integration_none">Select integration</option>
<option value="mikrotik">Mikrotik</option>
<option value="pfsense">pfSense</option>
<option value="opnsense">OPNsense</option>
</select>
<p class="text-xs text-gray-500 mt-1" data-i18n="settings.advanced.integration_hint">Choose where permanent bans should be synchronized.</p>
</div>
<div id="advancedMikrotikFields" class="hidden border border-gray-200 rounded-lg p-4 overflow-x-auto bg-gray-50">
<p class="text-sm text-gray-500" data-i18n="settings.advanced.mikrotik.note">Provide SSH credentials and the address list where IPs should be added.</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700" for="mikrotikHost" data-i18n="settings.advanced.mikrotik.host">Host</label>
<input id="mikrotikHost" type="text" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700" for="mikrotikPort" data-i18n="settings.advanced.mikrotik.port">Port</label>
<input id="mikrotikPort" type="number" min="1" max="65535" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500" placeholder="22">
</div>
<div>
<label class="block text-sm font-medium text-gray-700" for="mikrotikUsername" data-i18n="settings.advanced.mikrotik.username">SSH Username</label>
<input id="mikrotikUsername" type="text" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700" for="mikrotikPassword" data-i18n="settings.advanced.mikrotik.password">SSH Password</label>
<input id="mikrotikPassword" type="password" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700" for="mikrotikSSHKey" data-i18n="settings.advanced.mikrotik.key">SSH Key Path (optional)</label>
<input id="mikrotikSSHKey" type="text" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700" for="mikrotikList" data-i18n="settings.advanced.mikrotik.list">Address List Name</label>
<input id="mikrotikList" type="text" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500" placeholder="fail2ban-permanent">
</div>
</div>
</div>
<div id="advancedPfSenseFields" class="hidden border border-gray-200 rounded-lg p-4 overflow-x-auto bg-gray-50">
<p class="text-sm text-gray-500 mb-3" data-i18n="settings.advanced.pfsense.note">Requires the pfSense REST API package. Enter the API key and alias to manage.</p>
<div class="mb-3 text-sm">
<a href="https://github.com/jaredhendrickson13/pfsense-api/releases" target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:text-blue-800 underline" data-i18n="settings.advanced.pfsense.install_link">Install REST API Package</a>
<span class="text-gray-500 mx-2"></span>
<a href="https://pfrest.org/AUTHENTICATION_AND_AUTHORIZATION/" target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:text-blue-800 underline" data-i18n="settings.advanced.pfsense.api_key_setup">Setup API Key</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700" for="pfSenseBaseURL" data-i18n="settings.advanced.pfsense.base_url">Base URL</label>
<input id="pfSenseBaseURL" type="url" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500" placeholder="https://firewall.local">
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700" for="pfSenseToken" data-i18n="settings.advanced.pfsense.token">API Key</label>
<input id="pfSenseToken" type="text" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500" placeholder="Your API key from System > REST API > Keys">
<p class="text-xs text-gray-500 mt-1" data-i18n="settings.advanced.pfsense.token_hint">Generate in System > REST API > Keys in pfSense webConfigurator</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700" for="pfSenseAlias" data-i18n="settings.advanced.pfsense.alias">Alias Name</label>
<input id="pfSenseAlias" type="text" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex items-center">
<input type="checkbox" id="pfSenseSkipTLS" class="h-4 w-4 text-blue-600 border-gray-300 rounded">
<label for="pfSenseSkipTLS" class="ml-2 text-sm text-gray-700" data-i18n="settings.advanced.pfsense.skip_tls">Skip TLS verification (self-signed)</label>
</div>
</div>
</div>
<div id="advancedOPNsenseFields" class="hidden border border-gray-200 rounded-lg p-4 overflow-x-auto bg-gray-50">
<p class="text-sm text-gray-500 mb-3" data-i18n="settings.advanced.opnsense.note">Enter the OPNsense API credentials and alias to manage.</p>
<div class="mb-3 text-sm">
<a href="https://docs.opnsense.org/development/api.html" target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:text-blue-800 underline" data-i18n="settings.advanced.opnsense.api_docs">API Documentation</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700" for="opnsenseBaseURL" data-i18n="settings.advanced.opnsense.base_url">Base URL</label>
<input id="opnsenseBaseURL" type="url" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500" placeholder="https://firewall.local">
</div>
<div>
<label class="block text-sm font-medium text-gray-700" for="opnsenseKey" data-i18n="settings.advanced.opnsense.key">API Key</label>
<input id="opnsenseKey" type="text" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500" placeholder="Your API key">
<p class="text-xs text-gray-500 mt-1" data-i18n="settings.advanced.opnsense.key_hint">Generate in System > Access > Users > API Keys</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700" for="opnsenseSecret" data-i18n="settings.advanced.opnsense.secret">API Secret</label>
<input id="opnsenseSecret" type="text" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500" placeholder="Your API secret">
<p class="text-xs text-gray-500 mt-1" data-i18n="settings.advanced.opnsense.secret_hint">Generate together with API key</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700" for="opnsenseAlias" data-i18n="settings.advanced.opnsense.alias">Alias Name</label>
<input id="opnsenseAlias" type="text" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex items-center">
<input type="checkbox" id="opnsenseSkipTLS" class="h-4 w-4 text-blue-600 border-gray-300 rounded">
<label for="opnsenseSkipTLS" class="ml-2 text-sm text-gray-700" data-i18n="settings.advanced.opnsense.skip_tls">Skip TLS verification (self-signed)</label>
</div>
</div>
</div>
</div>
<div class="mt-6">
<h4 class="text-md font-semibold text-gray-800 mb-2" data-i18n="settings.advanced.log_title">Permanent Block Log</h4>
<div id="permanentBlockLog" class="overflow-x-auto border border-gray-200 rounded-md">
<p class="text-sm text-gray-500 p-4" data-i18n="settings.advanced.log_empty">No permanent blocks recorded yet.</p>
</div>
</div>
</div>
<!-- Alert Settings Group -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4" data-i18n="settings.alert">Alert Settings</h3>
<!-- Email Alert Preferences -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.email_alerts">Email Alert Preferences</label>
<div class="space-y-2">
<label class="flex items-center">
<input type="checkbox" id="emailAlertsForBans" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateEmailFieldsState()">
<span class="ml-2 text-sm text-gray-700" data-i18n="settings.email_alerts_for_bans">Enable email alerts for bans</span>
</label>
<label class="flex items-center">
<input type="checkbox" id="emailAlertsForUnbans" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateEmailFieldsState()">
<span class="ml-2 text-sm text-gray-700" data-i18n="settings.email_alerts_for_unbans">Enable email alerts for unbans</span>
</label>
</div>
</div>
<div class="mb-4" id="emailFieldsContainer">
<label for="destEmail" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.destination_email">Destination Email (Alerts Receiver)</label>
<input type="email" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" id="destEmail"
data-i18n-placeholder="settings.destination_email_placeholder" placeholder="alerts@swissmakers.ch" />
<p class="text-xs text-red-600 mt-1 hidden" id="destEmailError"></p>
</div>
<!-- GeoIP Provider -->
<div class="mb-4">
<label for="geoipProvider" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.geoip_provider">GeoIP Provider</label>
<select id="geoipProvider" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" onchange="onGeoIPProviderChange(this.value)">
<option value="builtin" data-i18n="settings.geoip_provider.builtin">Built-in (ip-api.com)</option>
<option value="maxmind" data-i18n="settings.geoip_provider.maxmind">MaxMind (Local Database)</option>
</select>
<p class="text-xs text-gray-500 mt-1" data-i18n="settings.geoip_provider.description">Choose the GeoIP lookup provider. MaxMind requires a local database file, while Built-in uses a free online API.</p>
</div>
<!-- GeoIP Database Path (shown only for MaxMind) -->
<div id="geoipDatabasePathContainer" class="mb-4">
<label for="geoipDatabasePath" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.geoip_database_path">GeoIP Database Path</label>
<input type="text" id="geoipDatabasePath" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="/usr/share/GeoIP/GeoLite2-Country.mmdb">
<p class="text-xs text-gray-500 mt-1" data-i18n="settings.geoip_database_path.description">Path to the MaxMind GeoLite2-Country database file.</p>
</div>
<!-- Max Log Lines -->
<div class="mb-4">
<label for="maxLogLines" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.max_log_lines">Maximum Log Lines</label>
<input type="number" id="maxLogLines" min="1" max="500" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="50">
<p class="text-xs text-gray-500 mt-1" data-i18n="settings.max_log_lines.description">Maximum number of log lines to include in ban notifications. Most relevant lines are selected automatically.</p>
</div>
<div class="mb-4">
<label for="alertCountries" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.alert_countries">Alert Countries</label>
<p class="text-sm text-gray-500 mb-2" data-i18n="settings.alert_countries_description">
Choose the countries for which you want to receive email alerts when a block is triggered.
</p>
<select id="alertCountries" class="w-full border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" multiple>
<option value="ALL">ALL (Every Country)</option>
<option value="AF">Afghanistan (AF)</option>
<option value="AL">Albania (AL)</option>
<option value="DZ">Algeria (DZ)</option>
<option value="AD">Andorra (AD)</option>
<option value="AO">Angola (AO)</option>
<option value="AG">Antigua and Barbuda (AG)</option>
<option value="AR">Argentina (AR)</option>
<option value="AM">Armenia (AM)</option>
<option value="AU">Australia (AU)</option>
<option value="AT">Austria (AT)</option>
<option value="AZ">Azerbaijan (AZ)</option>
<option value="BS">Bahamas (BS)</option>
<option value="BH">Bahrain (BH)</option>
<option value="BD">Bangladesh (BD)</option>
<option value="BB">Barbados (BB)</option>
<option value="BY">Belarus (BY)</option>
<option value="BE">Belgium (BE)</option>
<option value="BZ">Belize (BZ)</option>
<option value="BJ">Benin (BJ)</option>
<option value="BT">Bhutan (BT)</option>
<option value="BO">Bolivia (BO)</option>
<option value="BA">Bosnia and Herzegovina (BA)</option>
<option value="BW">Botswana (BW)</option>
<option value="BR">Brazil (BR)</option>
<option value="BN">Brunei (BN)</option>
<option value="BG">Bulgaria (BG)</option>
<option value="BF">Burkina Faso (BF)</option>
<option value="BI">Burundi (BI)</option>
<option value="CV">Cabo Verde (CV)</option>
<option value="KH">Cambodia (KH)</option>
<option value="CM">Cameroon (CM)</option>
<option value="CA">Canada (CA)</option>
<option value="CF">Central African Republic (CF)</option>
<option value="TD">Chad (TD)</option>
<option value="CL">Chile (CL)</option>
<option value="CN">China (CN)</option>
<option value="CO">Colombia (CO)</option>
<option value="KM">Comoros (KM)</option>
<option value="CG">Congo - Brazzaville (CG)</option>
<option value="CD">Congo - Kinshasa (CD)</option>
<option value="CR">Costa Rica (CR)</option>
<option value="CI">Côte d'Ivoire (CI)</option>
<option value="HR">Croatia (HR)</option>
<option value="CU">Cuba (CU)</option>
<option value="CY">Cyprus (CY)</option>
<option value="CZ">Czechia (CZ)</option>
<option value="DK">Denmark (DK)</option>
<option value="DJ">Djibouti (DJ)</option>
<option value="DM">Dominica (DM)</option>
<option value="DO">Dominican Republic (DO)</option>
<option value="EC">Ecuador (EC)</option>
<option value="EG">Egypt (EG)</option>
<option value="SV">El Salvador (SV)</option>
<option value="GQ">Equatorial Guinea (GQ)</option>
<option value="ER">Eritrea (ER)</option>
<option value="EE">Estonia (EE)</option>
<option value="SZ">Eswatini (SZ)</option>
<option value="ET">Ethiopia (ET)</option>
<option value="FJ">Fiji (FJ)</option>
<option value="FI">Finland (FI)</option>
<option value="FR">France (FR)</option>
<option value="GA">Gabon (GA)</option>
<option value="GM">Gambia (GM)</option>
<option value="GE">Georgia (GE)</option>
<option value="DE">Germany (DE)</option>
<option value="GH">Ghana (GH)</option>
<option value="GR">Greece (GR)</option>
<option value="GD">Grenada (GD)</option>
<option value="GT">Guatemala (GT)</option>
<option value="GN">Guinea (GN)</option>
<option value="GW">Guinea-Bissau (GW)</option>
<option value="GY">Guyana (GY)</option>
<option value="HT">Haiti (HT)</option>
<option value="HN">Honduras (HN)</option>
<option value="HU">Hungary (HU)</option>
<option value="IS">Iceland (IS)</option>
<option value="IN">India (IN)</option>
<option value="ID">Indonesia (ID)</option>
<option value="IR">Iran (IR)</option>
<option value="IQ">Iraq (IQ)</option>
<option value="IE">Ireland (IE)</option>
<option value="IL">Israel (IL)</option>
<option value="IT">Italy (IT)</option>
<option value="JM">Jamaica (JM)</option>
<option value="JP">Japan (JP)</option>
<option value="JO">Jordan (JO)</option>
<option value="KZ">Kazakhstan (KZ)</option>
<option value="KE">Kenya (KE)</option>
<option value="KI">Kiribati (KI)</option>
<option value="KP">North Korea (KP)</option>
<option value="KR">South Korea (KR)</option>
<option value="KW">Kuwait (KW)</option>
<option value="KG">Kyrgyzstan (KG)</option>
<option value="LA">Laos (LA)</option>
<option value="LV">Latvia (LV)</option>
<option value="LB">Lebanon (LB)</option>
<option value="LS">Lesotho (LS)</option>
<option value="LR">Liberia (LR)</option>
<option value="LY">Libya (LY)</option>
<option value="LI">Liechtenstein (LI)</option>
<option value="LT">Lithuania (LT)</option>
<option value="LU">Luxembourg (LU)</option>
<option value="MG">Madagascar (MG)</option>
<option value="MW">Malawi (MW)</option>
<option value="MY">Malaysia (MY)</option>
<option value="MV">Maldives (MV)</option>
<option value="ML">Mali (ML)</option>
<option value="MT">Malta (MT)</option>
<option value="MH">Marshall Islands (MH)</option>
<option value="MR">Mauritania (MR)</option>
<option value="MU">Mauritius (MU)</option>
<option value="MX">Mexico (MX)</option>
<option value="FM">Micronesia (FM)</option>
<option value="LOTR">Middle-earth (LOTR)</option>
<option value="MD">Moldova (MD)</option>
<option value="MC">Monaco (MC)</option>
<option value="MN">Mongolia (MN)</option>
<option value="ME">Montenegro (ME)</option>
<option value="MA">Morocco (MA)</option>
<option value="MZ">Mozambique (MZ)</option>
<option value="MM">Myanmar (MM)</option>
<option value="NA">Namibia (NA)</option>
<option value="NR">Nauru (NR)</option>
<option value="NP">Nepal (NP)</option>
<option value="NL">Netherlands (NL)</option>
<option value="NZ">New Zealand (NZ)</option>
<option value="NI">Nicaragua (NI)</option>
<option value="NE">Niger (NE)</option>
<option value="NG">Nigeria (NG)</option>
<option value="NO">Norway (NO)</option>
<option value="OM">Oman (OM)</option>
<option value="PK">Pakistan (PK)</option>
<option value="PW">Palau (PW)</option>
<option value="PS">Palestine (PS)</option>
<option value="PA">Panama (PA)</option>
<option value="PG">Papua New Guinea (PG)</option>
<option value="PY">Paraguay (PY)</option>
<option value="PE">Peru (PE)</option>
<option value="PH">Philippines (PH)</option>
<option value="PL">Polland (PL)</option>
<option value="PT">Portugal (PT)</option>
<option value="QA">Qatar (QA)</option>
<option value="RO">Romania (RO)</option>
<option value="RU">Russia (RU)</option>
<option value="RW">Rwanda (RW)</option>
<option value="KN">Saint Kitts and Nevis (KN)</option>
<option value="LC">Saint Lucia (LC)</option>
<option value="VC">Saint Vincent and the Grenadines (VC)</option>
<option value="WS">Samoa (WS)</option>
<option value="SM">San Marino (SM)</option>
<option value="ST">Sao Tome and Principe (ST)</option>
<option value="SA">Saudi Arabia (SA)</option>
<option value="SN">Senegal (SN)</option>
<option value="RS">Serbia (RS)</option>
<option value="SC">Seychelles (SC)</option>
<option value="SL">Sierra Leone (SL)</option>
<option value="SG">Singapore (SG)</option>
<option value="SK">Slovakia (SK)</option>
<option value="SI">Slovenia (SI)</option>
<option value="SB">Solomon Islands (SB)</option>
<option value="SO">Somalia (SO)</option>
<option value="ZA">South Africa (ZA)</option>
<option value="SS">South Sudan (SS)</option>
<option value="ES">Spain (ES)</option>
<option value="LK">Sri Lanka (LK)</option>
<option value="SD">Sudan (SD)</option>
<option value="SR">Suriname (SR)</option>
<option value="SE">Sweden (SE)</option>
<option value="CH">Switzerland (CH)</option>
<option value="SY">Syria (SY)</option>
<option value="TW">Taiwan (TW)</option>
<option value="TJ">Tajikistan (TJ)</option>
<option value="TZ">Tanzania (TZ)</option>
<option value="TH">Thailand (TH)</option>
<option value="TL">Timor-Leste (TL)</option>
<option value="TG">Togo (TG)</option>
<option value="TO">Tonga (TO)</option>
<option value="TT">Trinidad and Tobago (TT)</option>
<option value="TN">Tunisia (TN)</option>
<option value="TR">Turkey (TR)</option>
<option value="TM">Turkmenistan (TM)</option>
<option value="TV">Tuvalu (TV)</option>
<option value="UG">Uganda (UG)</option>
<option value="UA">Ukraine (UA)</option>
<option value="AE">United Arab Emirates (AE)</option>
<option value="GB">United Kingdom (GB)</option>
<option value="US">United States (US)</option>
<option value="UY">Uruguay (UY)</option>
<option value="UZ">Uzbekistan (UZ)</option>
<option value="VU">Vanuatu (VU)</option>
<option value="VE">Venezuela (VE)</option>
<option value="VN">Vietnam (VN)</option>
<option value="YE">Yemen (YE)</option>
<option value="ZM">Zambia (ZM)</option>
<option value="ZW">Zimbabwe (ZW)</option>
</select>
</div>
</div>
<!-- SMTP Configuration Group -->
<div class="bg-white rounded-lg shadow p-6" id="smtpFieldsContainer">
<h3 class="text-lg font-medium text-gray-900 mb-4" data-i18n="settings.smtp">SMTP Configuration</h3>
<div class="mb-4">
<label for="smtpHost" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.smtp_host">SMTP Host</label>
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" id="smtpHost"
data-i18n-placeholder="settings.smtp_host_placeholder" placeholder="e.g., smtp.gmail.com" required />
</div>
<div class="mb-4">
<label for="smtpPort" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.smtp_port">SMTP Port</label>
<select id="smtpPort" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed">
<option value="587" selected>587 (Recommended - STARTTLS)</option>
<option value="465" disabled>465 (Not Supported)</option>
</select>
</div>
<div class="mb-4">
<label for="smtpUsername" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.smtp_username">SMTP Username</label>
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" id="smtpUsername"
data-i18n-placeholder="settings.smtp_username_placeholder" placeholder="e.g., user@example.com" required />
</div>
<div class="mb-4">
<label for="smtpPassword" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.smtp_password">SMTP Password</label>
<input type="password" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" id="smtpPassword"
data-i18n-placeholder="settings.smtp_password_placeholder" placeholder="Enter SMTP Password" required />
</div>
<div class="mb-4">
<label for="smtpFrom" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.smtp_sender">Sender Email</label>
<input type="email" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" id="smtpFrom"
data-i18n-placeholder="settings.smtp_sender_placeholder" placeholder="noreply@swissmakers.ch" required />
</div>
<div class="flex items-center mb-4">
<input type="checkbox" id="smtpUseTLS" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed">
<label for="smtpUseTLS" class="ml-2 block text-sm text-gray-700" data-i18n="settings.smtp_tls">Use TLS (Recommended)</label>
</div>
<button type="button" class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed" onclick="sendTestEmail()" id="sendTestEmailBtn" data-i18n="settings.send_test_email">Send Test Email</button>
</div>
<!-- Fail2Ban Configuration Group -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4" data-i18n="settings.fail2ban">Global Default Fail2Ban Configurations</h3>
<p class="text-sm text-gray-600 mb-4" data-i18n="settings.fail2ban.description">These settings will be applied to all enabled Fail2Ban servers and stored in their jail.local [DEFAULT] section.</p>
<!-- Bantime Increment -->
<div class="mb-4">
<div class="flex items-center mb-2">
<input type="checkbox" id="bantimeIncrement" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out" />
<label for="bantimeIncrement" class="ml-2 block text-sm font-medium text-gray-700" data-i18n="settings.enable_bantime_increment">Enable Bantime Increment</label>
</div>
<p class="text-xs text-gray-500 ml-9" data-i18n="settings.enable_bantime_increment.description">If set to true, the bantime will be calculated using the formula: bantime = findtime * (number of failures / maxretry) * (1 + bantime.rndtime).</p>
</div>
2025-12-15 18:57:50 +01:00
<!-- Default Enabled -->
<div class="mb-4">
<div class="flex items-center mb-2">
<input type="checkbox" id="defaultJailEnable" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out" />
<label for="defaultJailEnable" class="ml-2 block text-sm font-medium text-gray-700" data-i18n="settings.default_jail_enable">Enable Jails by Default</label>
</div>
<p class="text-xs text-gray-500 ml-9" data-i18n="settings.default_jail_enable.description">If enabled, all jails will be enabled by default. When disabled, jails must be explicitly enabled.</p>
</div>
<!-- Bantime -->
<div class="mb-4">
<label for="banTime" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.default_bantime">Default Bantime</label>
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.default_bantime.description">The number of seconds that a host is banned. Time format: 1h = 1 hour, 1d = 1 day, 1w = 1 week, 1m = 1 month, 1y = 1 year.</p>
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="banTime"
data-i18n-placeholder="settings.default_bantime_placeholder" placeholder="e.g., 48h" />
<p class="text-xs text-red-600 mt-1 hidden" id="banTimeError"></p>
</div>
<!-- Banaction -->
<div class="mb-4">
<label for="banaction" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.banaction">Banaction</label>
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.banaction.description">Default banning action (e.g. nftables-multiport, nftables-allports, firewallcmd-rich-rules, etc). It is used to define action_* variables.</p>
<select id="banaction" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="firewallcmd-multiport">firewallcmd-multiport</option>
<option value="firewallcmd-allports">firewallcmd-allports</option>
<option value="firewallcmd-ipset">firewallcmd-ipset</option>
<option value="firewallcmd-new">firewallcmd-new</option>
<option value="firewallcmd-rich-rules">firewallcmd-rich-rules</option>
<option value="firewallcmd-rich-logging">firewallcmd-rich-logging</option>
<option value="nftables-multiport">nftables-multiport</option>
<option value="nftables-allports">nftables-allports</option>
<option value="nftables">nftables</option>
<option value="iptables-multiport">iptables-multiport</option>
<option value="iptables-allports">iptables-allports</option>
<option value="iptables-new">iptables-new</option>
<option value="iptables-ipset">iptables-ipset</option>
<option value="iptables-ipset-proto4">iptables-ipset-proto4</option>
<option value="iptables-ipset-proto6">iptables-ipset-proto6</option>
<option value="iptables-ipset-proto6-allports">iptables-ipset-proto6-allports</option>
<option value="iptables-multiport-log">iptables-multiport-log</option>
<option value="iptables-xt_recent-echo">iptables-xt_recent-echo</option>
<option value="shorewall">shorewall</option>
<option value="shorewall-ipset-proto6">shorewall-ipset-proto6</option>
<option value="ufw">ufw</option>
<option value="pf">pf</option>
<option value="bsd-ipfw">bsd-ipfw</option>
<option value="ipfw">ipfw</option>
<option value="ipfilter">ipfilter</option>
<option value="npf">npf</option>
<option value="osx-ipfw">osx-ipfw</option>
<option value="osx-afctl">osx-afctl</option>
<option value="apf">apf</option>
</select>
</div>
<!-- Banaction Allports -->
<div class="mb-4">
<label for="banactionAllports" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.banaction_allports">Banaction Allports</label>
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.banaction_allports.description">Banning action for all ports (e.g. iptables-allports, firewallcmd-allports, etc). Used when a jail needs to ban all ports instead of specific ones.</p>
<select id="banactionAllports" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="firewallcmd-allports">firewallcmd-allports</option>
<option value="firewallcmd-multiport">firewallcmd-multiport</option>
<option value="firewallcmd-ipset">firewallcmd-ipset</option>
<option value="firewallcmd-new">firewallcmd-new</option>
<option value="firewallcmd-rich-rules">firewallcmd-rich-rules</option>
<option value="firewallcmd-rich-logging">firewallcmd-rich-logging</option>
<option value="nftables-allports">nftables-allports</option>
<option value="nftables-multiport">nftables-multiport</option>
<option value="nftables">nftables</option>
<option value="iptables-allports">iptables-allports</option>
<option value="iptables-multiport">iptables-multiport</option>
<option value="iptables-new">iptables-new</option>
<option value="iptables-ipset">iptables-ipset</option>
<option value="iptables-ipset-proto4">iptables-ipset-proto4</option>
<option value="iptables-ipset-proto6">iptables-ipset-proto6</option>
<option value="iptables-ipset-proto6-allports">iptables-ipset-proto6-allports</option>
<option value="iptables-multiport-log">iptables-multiport-log</option>
<option value="iptables-xt_recent-echo">iptables-xt_recent-echo</option>
<option value="shorewall">shorewall</option>
<option value="shorewall-ipset-proto6">shorewall-ipset-proto6</option>
<option value="ufw">ufw</option>
<option value="pf">pf</option>
<option value="bsd-ipfw">bsd-ipfw</option>
<option value="ipfw">ipfw</option>
<option value="ipfilter">ipfilter</option>
<option value="npf">npf</option>
<option value="osx-ipfw">osx-ipfw</option>
<option value="osx-afctl">osx-afctl</option>
<option value="apf">apf</option>
</select>
</div>
<!-- Findtime -->
<div class="mb-4">
<label for="findTime" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.default_findtime">Default Findtime</label>
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.default_findtime.description">A host is banned if it has generated 'maxretry' failures during the last 'findtime' seconds. Time format: 1h = 1 hour, 1d = 1 day, 1w = 1 week, 1m = 1 month, 1y = 1 year.</p>
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="findTime"
data-i18n-placeholder="settings.default_findtime_placeholder" placeholder="e.g., 30m" />
<p class="text-xs text-red-600 mt-1 hidden" id="findTimeError"></p>
</div>
<!-- Max Retry -->
<div class="mb-4">
<label for="maxRetry" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.default_max_retry">Default Max Retry</label>
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.default_max_retry.description">Number of failures before a host gets banned.</p>
<input type="number" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="maxRetry"
data-i18n-placeholder="settings.default_max_retry_placeholder" placeholder="Enter maximum retries" min="1" />
<p class="text-xs text-red-600 mt-1 hidden" id="maxRetryError"></p>
</div>
<!-- Ignore IPs -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.ignore_ips">Ignore IPs</label>
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.ignore_ips.description">Space separated list of IP addresses, CIDR masks or DNS hosts. Fail2ban will not ban a host which matches an address in this list.</p>
<div class="border border-gray-300 rounded-md p-2 min-h-[60px] bg-gray-50" id="ignoreIPsContainer">
<div id="ignoreIPsTags" class="flex flex-wrap gap-2 mb-2"></div>
<input type="text" id="ignoreIPInput" class="w-full border-0 bg-transparent focus:outline-none focus:ring-0 text-sm"
data-i18n-placeholder="settings.ignore_ips_placeholder" placeholder="Enter IP address and press Enter" />
<div id="ignoreIPsError" class="hidden text-red-600 text-sm mt-1"></div>
</div>
</div>
</div>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors" data-i18n="settings.save">Save</button>
</form>
2025-07-19 11:19:40 +02:00
</div>
2025-07-19 11:19:40 +02:00
<!-- *********************** Settings Page END ************************* -->
</main>
<!-- Footer -->
<footer id="footer" class="hidden bg-gray-100 py-4">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-gray-600 text-sm">
<p class="mb-0">
&copy; <a href="https://swissmakers.ch" target="_blank" class="text-blue-600 hover:text-blue-800">Swissmakers GmbH</a>
-
<a href="https://github.com/swissmakers/fail2ban-ui" target="_blank" class="text-blue-600 hover:text-blue-800">GitHub</a>
</p>
2025-01-25 16:21:14 +01:00
</div>
</footer>
2025-07-19 11:19:40 +02:00
<!-- ******************************************************************* -->
<!-- Modal Templates START -->
<!-- ******************************************************************* -->
<!-- Jail Config Modal -->
<div id="jailConfigModal" class="hidden fixed inset-0 z-50 overflow-y-auto" style="z-index: 60;">
<div class="relative flex min-h-full w-full items-center justify-center p-2 sm:p-4">
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all my-4 sm:my-8" style="max-width: 90vw; max-height: calc(100vh - 2rem); display: flex; flex-direction: column;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4" style="flex: 1; overflow-y: auto; min-height: 0;">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<div class="flex items-center justify-between">
<h3 class="text-lg leading-6 font-medium text-gray-900">
<span data-i18n="modal.filter_config">Filter Config:</span> <span id="modalJailName"></span>
</h3>
<button type="button" onclick="closeModal('jailConfigModal')" class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600" aria-label="Close">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="mt-4 space-y-4">
<!-- Filter Configuration -->
<div>
<div class="flex items-center justify-between mb-2">
<label class="block text-sm font-medium text-gray-700" data-i18n="modal.filter_config_label">Filter Configuration</label>
<span id="filterFilePath" class="text-xs text-gray-500 font-mono" style="display: none;"></span>
</div>
<div class="relative" style="position: relative;">
<textarea id="filterConfigTextarea"
class="w-full border border-gray-700 rounded-md px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 h-96 font-mono text-sm bg-gray-900 text-white resize-none overflow-auto"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
data-lpignore="true"
data-1p-ignore="true"
data-bwignore="true"
data-form-type="other"
data-extension-ignore="true"
data-icloud-keychain-ignore="true"
data-safari-autofill="false"
role="textbox"
aria-label="Filter configuration editor"
name="filter-config-editor"
inputmode="text"
style="caret-color: #ffffff; line-height: 1.5; tab-size: 2; width: 100%; min-width: 100%; max-width: 100%; box-sizing: border-box; -webkit-appearance: none; appearance: none; position: relative; z-index: 2;"
wrap="off"
onfocus="preventExtensionInterference(this);"></textarea>
</div>
</div>
<!-- Divider -->
<div class="border-t border-gray-300"></div>
<!-- Jail Configuration -->
<div>
<div class="flex items-center justify-between mb-2">
<label class="block text-sm font-medium text-gray-700" data-i18n="modal.jail_config_label">Jail Configuration</label>
<span id="jailFilePath" class="text-xs text-gray-500 font-mono" style="display: none;"></span>
</div>
<div class="relative" style="position: relative;">
<textarea id="jailConfigTextarea"
class="w-full border border-gray-700 rounded-md px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm bg-gray-900 text-white resize-none overflow-auto"
spellcheck="false"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
data-lpignore="true"
data-1p-ignore="true"
data-bwignore="true"
data-form-type="other"
data-extension-ignore="true"
data-icloud-keychain-ignore="true"
data-safari-autofill="false"
role="textbox"
aria-label="Jail configuration editor"
name="jail-config-editor"
inputmode="text"
style="height: 300px; caret-color: #ffffff; line-height: 1.5; tab-size: 2; width: 100%; min-width: 100%; max-width: 100%; box-sizing: border-box; -webkit-appearance: none; appearance: none; position: relative; z-index: 2;"
wrap="off"
onfocus="preventExtensionInterference(this);"></textarea>
</div>
</div>
<!-- Test Logpath Button (only shown if logpath is set) -->
<div id="testLogpathSection" class="hidden">
<div id="localServerLogpathHint" class="mb-2 p-2 bg-blue-50 border border-blue-200 rounded-md text-xs text-blue-800 hidden">
<strong data-i18n="modal.local_server_logpath_note"> Note:</strong> <span data-i18n="modal.local_server_logpath_text_prefix">For a local fail2ban server (e.g. installed on container host system or in a container on same host), log files must also be mounted to the fail2ban-ui container (e.g.,</span> <code class="font-mono">-v /var/log:/var/log:ro</code> <span data-i18n="modal.local_server_logpath_text_suffix">) this is required so that the fail2ban-ui can verify logpath variables or paths when updating jails.</span>
</div>
<button type="button"
class="inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-600 text-sm font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
onclick="testLogpath()"
data-i18n="modal.test_logpath">Test Logpath</button>
<div id="logpathResults" class="mt-2 p-3 bg-gray-100 rounded-md text-sm max-h-64 overflow-y-auto hidden"></div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse" style="flex-shrink: 0;">
<button type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm" onclick="saveJailConfig()" data-i18n="modal.save">Save</button>
<button type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" onclick="closeModal('jailConfigModal')" data-i18n="modal.cancel">Cancel</button>
</div>
</div>
</div>
2025-01-27 11:35:21 +01:00
</div>
<!-- Manage Jails Modal -->
<div id="manageJailsModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 1000px;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<div class="flex items-center justify-between">
<h3 class="text-lg leading-6 font-medium text-gray-900" data-i18n="modal.manage_jails_title">Manage Jails</h3>
<button type="button" onclick="closeModal('manageJailsModal')" class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600" aria-label="Close">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="mt-4">
<div class="mb-4 flex justify-end gap-2">
<button type="button" onclick="openCreateFilterModal()" class="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 text-white text-sm font-medium rounded-md hover:bg-gray-700 transition-colors">
<i class="fas fa-plus"></i>
<span data-i18n="modal.create_filter">Create New Filter</span>
</button>
<button type="button" onclick="openCreateJailModal()" class="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 text-white text-sm font-medium rounded-md hover:bg-gray-700 transition-colors">
<i class="fas fa-plus"></i>
<span data-i18n="modal.create_jail">Create New Jail</span>
</button>
</div>
<!-- Dynamically filled list of jails with toggle switches -->
<div id="jailsList" class="divide-y divide-gray-200"></div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" onclick="closeModal('manageJailsModal')" data-i18n="modal.close">Close</button>
</div>
</div>
</div>
</div>
<!-- Create Jail Modal -->
<div id="createJailModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 600px;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<div class="flex items-center justify-between">
<h3 class="text-lg leading-6 font-medium text-gray-900" data-i18n="modal.create_jail_title">Create New Jail</h3>
<button type="button" onclick="closeModal('createJailModal')" class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600" aria-label="Close">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="mt-4">
<div class="mb-4">
<label for="newJailName" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="modal.jail_name">Jail Name</label>
<input type="text" id="newJailName" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="e.g., sshd" />
<p class="text-xs text-gray-500 mt-1" data-i18n="modal.jail_name_hint">Only alphanumeric characters, dashes, and underscores are allowed.</p>
</div>
<div class="mb-4">
<label for="newJailFilter" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="modal.jail_filter">Filter (optional)</label>
<select id="newJailFilter" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" onchange="updateJailConfigFromFilter()">
<option value="">-- Select a filter --</option>
</select>
<p class="text-xs text-gray-500 mt-1" data-i18n="modal.jail_filter_hint">Selecting a filter will auto-populate the jail configuration.</p>
</div>
<div class="mb-4">
<label for="newJailContent" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="modal.jail_config">Jail Configuration</label>
<textarea id="newJailContent" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 h-40 font-mono text-sm" placeholder="[jailname]&#10;enabled = false&#10;port = ssh&#10;filter = sshd&#10;logpath = /var/log/auth.log"></textarea>
<p class="text-xs text-gray-500 mt-1" data-i18n="modal.jail_config_hint">Jail configuration will be auto-populated when you select a filter.</p>
</div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" onclick="createJail()" class="mt-3 w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-600 text-base font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" data-i18n="modal.create">Create</button>
<button type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" onclick="closeModal('createJailModal')" data-i18n="modal.cancel">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Create Filter Modal -->
<div id="createFilterModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 600px;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<div class="flex items-center justify-between">
<h3 class="text-lg leading-6 font-medium text-gray-900" data-i18n="modal.create_filter_title">Create New Filter</h3>
<button type="button" onclick="closeModal('createFilterModal')" class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600" aria-label="Close">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="mt-4">
<div class="mb-4">
<label for="newFilterName" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="modal.filter_name">Filter Name</label>
<input type="text" id="newFilterName" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="e.g., myfilter" />
<p class="text-xs text-gray-500 mt-1" data-i18n="modal.filter_name_hint">Only alphanumeric characters, dashes, and underscores are allowed.</p>
</div>
<div class="mb-4">
<label for="newFilterContent" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="modal.filter_config">Filter Configuration (optional)</label>
<textarea id="newFilterContent" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 h-40 font-mono text-sm" placeholder="# Filter: myfilter&#10;[Definition]&#10;failregex = ^.*Failed login.*$"></textarea>
<p class="text-xs text-gray-500 mt-1" data-i18n="modal.filter_config_hint">If left empty, an empty filter file will be created.</p>
</div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" onclick="createFilter()" class="mt-3 w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-600 text-base font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" data-i18n="modal.create">Create</button>
<button type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" onclick="closeModal('createFilterModal')" data-i18n="modal.cancel">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Server Manager Modal -->
<div id="serverManagerModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 1000px;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-6">
<div class="flex flex-col gap-6">
<div>
<div class="flex items-center justify-between">
<h3 class="text-lg leading-6 font-medium text-gray-900" data-i18n="servers.modal.title">Manage Fail2ban Servers</h3>
<button type="button" onclick="closeModal('serverManagerModal')" class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600" aria-label="Close">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p class="mt-1 text-sm text-gray-500" data-i18n="servers.modal.description">
Register remote Fail2ban instances and choose how the UI connects to them.
</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h4 class="text-md font-semibold text-gray-800 mb-3" data-i18n="servers.modal.list_title">Registered Servers</h4>
<div id="serverManagerList" class="space-y-3 max-h-96 overflow-y-auto pr-1"></div>
<div id="serverManagerListEmpty" class="text-sm text-gray-500 hidden" data-i18n="servers.modal.list_empty">
No servers configured yet. Add your first Fail2ban server using the form on the right.
</div>
</div>
<div>
<div class="flex items-center justify-between mb-3">
<h4 class="text-md font-semibold text-gray-800" data-i18n="servers.modal.form_title">Add or Update Server</h4>
<button type="button" onclick="resetServerForm()" class="text-sm text-blue-600 hover:text-blue-800 font-medium" data-i18n="servers.form.new_server">New Server</button>
</div>
<form id="serverForm" class="space-y-4" onsubmit="submitServerForm(event)">
<div>
<label for="serverName" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.name">Display Name</label>
<input type="text" id="serverName" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required data-i18n-placeholder="servers.form.name_placeholder" placeholder="My Fail2ban server">
</div>
<div>
<label for="serverType" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.type">Connection Type</label>
<select id="serverType" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required onchange="onServerTypeChange(this.value)">
<option value="local" data-i18n="servers.type.local">Local (same host)</option>
<option value="ssh" data-i18n="servers.type.ssh">SSH</option>
<option value="agent" data-i18n="servers.type.agent">API Agent</option>
</select>
</div>
<div data-server-fields="ssh agent">
<label for="serverHost" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.host">Hostname / IP</label>
<input type="text" id="serverHost" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.host_placeholder" placeholder="fail2ban.example.com">
</div>
<div data-server-fields="ssh agent">
<label for="serverPort" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.port">Port</label>
<input type="number" id="serverPort" min="1" max="65535" value="22" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.port_placeholder" placeholder="22">
</div>
<div data-server-fields="local ssh">
<label for="serverSocket" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.socket_path">Fail2ban Socket Path</label>
<input type="text" id="serverSocket" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.socket_path_placeholder" placeholder="/var/run/fail2ban/fail2ban.sock">
</div>
<div data-server-fields="local">
<label for="serverLogPath" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.log_path">Fail2ban Log Path</label>
<input type="text" id="serverLogPath" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.log_path_placeholder" placeholder="/var/log/fail2ban.log">
</div>
<div data-server-fields="local">
<label for="serverHostname" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.hostname">Server Hostname</label>
<input type="text" id="serverHostname" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.hostname_placeholder" placeholder="optional">
</div>
<div data-server-fields="ssh">
<label for="serverSSHUser" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.ssh_user">SSH User</label>
<input type="text" id="serverSSHUser" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.ssh_user_placeholder" placeholder="sa_fail2ban">
</div>
<div data-server-fields="ssh">
<label for="serverSSHKey" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.ssh_key">SSH Private Key Path</label>
<input type="text" id="serverSSHKey" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.ssh_key_placeholder" placeholder="/config/.ssh/id_rsa">
<p class="mt-1 text-sm text-gray-500" data-i18n="servers.form.ssh_key_help">
Place your SSH private key in the <code class="px-1 py-0.5 bg-gray-100 rounded text-xs">/config/.ssh/</code> directory (mounted config volume).
The key file must have permissions <code class="px-1 py-0.5 bg-gray-100 rounded text-xs">600</code> (chmod 600).
Example: <code class="px-1 py-0.5 bg-gray-100 rounded text-xs">/config/.ssh/id_rsa</code>
</p>
</div>
<div data-server-fields="ssh">
<label for="serverSSHKeySelect" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.select_key">Select Private Key</label>
<select id="serverSSHKeySelect" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="" data-i18n="servers.form.select_key_placeholder">Manual entry</option>
</select>
</div>
<div data-server-fields="agent">
<label for="serverAgentUrl" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.agent_url">Agent URL</label>
<input type="url" id="serverAgentUrl" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.agent_url_placeholder" placeholder="https://host:9443">
</div>
<div data-server-fields="agent">
<label for="serverAgentSecret" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.agent_secret">Agent Secret</label>
<input type="text" id="serverAgentSecret" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.agent_secret_placeholder" placeholder="shared secret token">
</div>
<div>
<label for="serverTags" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="servers.form.tags">Tags</label>
<input type="text" id="serverTags" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="servers.form.tags_placeholder" placeholder="comma,separated,tags">
</div>
<div class="flex items-center">
<input type="checkbox" id="serverDefault" class="h-4 w-4 text-blue-600 border-gray-300 rounded">
<label for="serverDefault" class="ml-2 text-sm text-gray-700" data-i18n="servers.form.set_default">Set as default server</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="serverEnabled" class="h-4 w-4 text-blue-600 border-gray-300 rounded">
<label for="serverEnabled" class="ml-2 text-sm text-gray-700" data-i18n="servers.form.enabled">Enable connector</label>
</div>
<input type="hidden" id="serverId">
<div class="flex gap-3">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors" data-i18n="servers.form.submit">Save Server</button>
<button type="button" class="px-4 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors" onclick="resetServerForm()" data-i18n="servers.form.reset">Reset</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" class="w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm" onclick="closeModal('serverManagerModal')" data-i18n="modal.close">Close</button>
</div>
</div>
</div>
</div>
<!-- Whois Modal -->
<div id="whoisModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 800px;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<div class="flex items-center justify-between">
<h3 class="text-lg leading-6 font-medium text-gray-900">
<span data-i18n="logs.modal.whois_title">Whois Information</span> - <span id="whoisModalIP"></span>
</h3>
<button type="button" onclick="closeModal('whoisModal')" class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600" aria-label="Close">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="mt-4">
<pre id="whoisModalContent" class="w-full border border-gray-300 rounded-md px-3 py-2 bg-gray-900 text-white font-mono text-xs overflow-x-auto" style="max-height: 70vh; white-space: pre-wrap; word-wrap: break-word;"></pre>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" class="w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm" onclick="closeModal('whoisModal')" data-i18n="modal.close">Close</button>
</div>
</div>
</div>
</div>
<!-- Advanced Actions Test Modal -->
<div id="advancedTestModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 800px;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<div class="flex items-center justify-between">
<h3 class="text-lg leading-6 font-medium text-gray-900" data-i18n="settings.advanced.test_title">Manually Block / Test</h3>
<button type="button" onclick="closeModal('advancedTestModal')" class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600" aria-label="Close">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="mt-4 space-y-4">
<div>
<label for="advancedTestIP" class="block text-sm font-medium text-gray-700" data-i18n="settings.advanced.test_ip">IP address</label>
<input type="text" id="advancedTestIP" class="mt-1 w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="203.0.113.10">
</div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse gap-3">
<button type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm" onclick="submitAdvancedTest('block')" data-i18n="settings.advanced.test_block">Block IP</button>
<button type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" onclick="closeModal('advancedTestModal')" data-i18n="modal.close">Close</button>
</div>
</div>
</div>
</div>
<!-- Logs Modal -->
<div id="logsModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 1200px;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg leading-6 font-medium text-gray-900">
<span data-i18n="logs.modal.logs_title">Logs</span> - <span id="logsModalIP"></span>
</h3>
<button type="button" onclick="closeModal('logsModal')" class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600" aria-label="Close">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p class="text-sm text-gray-600 mb-4">
<span data-i18n="logs.modal.jail">Jail:</span> <span id="logsModalJail" class="font-semibold"></span>
</p>
<div class="mt-4">
<pre id="logsModalContent" class="w-full border border-gray-300 rounded-md px-3 py-2 bg-gray-900 text-white font-mono text-xs overflow-x-auto" style="max-height: 70vh; white-space: pre-wrap; word-wrap: break-word;"></pre>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" class="w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm" onclick="closeModal('logsModal')" data-i18n="modal.close">Close</button>
</div>
</div>
</div>
</div>
<!-- Ban Insights Modal -->
<div id="banInsightsModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 1200px;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg leading-6 font-medium text-gray-900" data-i18n="logs.modal.insights_title">Ban Insights</h3>
<button type="button" onclick="closeModal('banInsightsModal')" class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600" aria-label="Close">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p class="text-sm text-gray-600 mb-4" data-i18n="logs.modal.insights_description">Country distribution and recurring offenders.</p>
<!-- Summary Cards -->
<div id="insightsSummary" class="grid gap-4 sm:grid-cols-3 mb-6"></div>
<!-- Main Content Grid -->
<div class="grid gap-6 lg:grid-cols-2">
<!-- Country Statistics -->
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50">
<div class="flex items-center justify-between mb-4">
<div>
<h4 class="text-base font-semibold text-gray-900" data-i18n="logs.modal.insights_countries">Bans by country</h4>
<p class="text-xs text-gray-500 mt-1" data-i18n="logs.modal.insights_countries_hint">Top origins for the selected time range.</p>
</div>
<span class="inline-flex items-center rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-700">Geo</span>
</div>
<div id="countryStatsContainer" class="space-y-4 max-h-96 overflow-y-auto"></div>
</div>
<!-- Recurring IPs -->
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50">
<div class="flex items-center justify-between mb-4">
<div>
<h4 class="text-base font-semibold text-gray-900" data-i18n="logs.modal.insights_recurring">Recurring IPs</h4>
<p class="text-xs text-gray-500 mt-1" data-i18n="logs.modal.insights_recurring_hint">IP addresses repeatedly triggering Fail2ban.</p>
</div>
<span class="inline-flex items-center rounded-full bg-amber-100 px-3 py-1 text-xs font-medium text-amber-700">Watchlist</span>
</div>
<div id="recurringIPsContainer" class="space-y-4 max-h-96 overflow-y-auto"></div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" class="w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm" onclick="closeModal('banInsightsModal')" data-i18n="modal.close">Close</button>
</div>
</div>
</div>
</div>
2025-07-19 11:19:40 +02:00
<!-- ********************** Modal Templates END ************************ -->
<!-- jQuery (used by Select2) -->
<script src="/static/vendor/jquery/jquery-3.6.0.min.js?v={{.version}}"></script>
<!-- Select2 JS -->
<script src="/static/vendor/select2/select2.min.js?v={{.version}}"></script>
<!-- Fail2ban UI JavaScript Modules -->
<script src="/static/js/globals.js?v={{.version}}"></script>
<script src="/static/js/core.js?v={{.version}}"></script>
<script src="/static/js/api.js?v={{.version}}"></script>
<script src="/static/js/utils.js?v={{.version}}"></script>
<script src="/static/js/validation.js?v={{.version}}"></script>
<script src="/static/js/modals.js?v={{.version}}"></script>
<script src="/static/js/translations.js?v={{.version}}"></script>
<script src="/static/js/ignoreips.js?v={{.version}}"></script>
<script src="/static/js/dashboard.js?v={{.version}}"></script>
<script src="/static/js/servers.js?v={{.version}}"></script>
<script src="/static/js/jails.js?v={{.version}}"></script>
<script src="/static/js/console.js?v={{.version}}"></script>
<script src="/static/js/settings.js?v={{.version}}"></script>
<script src="/static/js/filters.js?v={{.version}}"></script>
<script src="/static/js/websocket.js?v={{.version}}"></script>
<script src="/static/js/header.js?v={{.version}}"></script>
<script src="/static/js/lotr.js?v={{.version}}"></script>
<script src="/static/js/auth.js?v={{.version}}"></script>
<script src="/static/js/init.js?v={{.version}}"></script>
2025-01-25 16:21:14 +01:00
</body>
2025-08-19 19:11:10 +02:00
</html>