Move bad log parsing to utils.js and add section headers

This commit is contained in:
2026-02-16 22:58:58 +01:00
parent 4962d398a1
commit 0d551ede53
7 changed files with 179 additions and 249 deletions

View File

@@ -1,10 +1,17 @@
// Header components: Clock and Backend Status Indicator // Header components: Clock and Backend Status Indicator
"use strict"; "use strict";
// =========================================================================
// Global Variables
// =========================================================================
var clockInterval = null; var clockInterval = null;
var statusUpdateCallback = null; var statusUpdateCallback = null;
// Initialize clock // =========================================================================
// Clock
// =========================================================================
function initClock() { function initClock() {
function updateClock() { function updateClock() {
var now = new Date(); var now = new Date();
@@ -18,30 +25,59 @@ function initClock() {
clockElement.textContent = timeString; clockElement.textContent = timeString;
} }
} }
// Update immediately
updateClock(); updateClock();
// Update every second
if (clockInterval) { if (clockInterval) {
clearInterval(clockInterval); clearInterval(clockInterval);
} }
clockInterval = setInterval(updateClock, 1000); clockInterval = setInterval(updateClock, 1000);
} }
// Update status indicator // =========================================================================
// Status Indicator
// =========================================================================
function initStatusIndicator() {
updateStatusIndicator('connecting', 'Connecting...');
function registerStatusCallback() {
if (typeof wsManager !== 'undefined' && wsManager) {
wsManager.onStatusChange(function(state, text) {
updateStatusIndicator(state, text);
});
var currentState = wsManager.getConnectionState();
var currentText = 'Connecting...';
if (currentState === 'connected' && wsManager.isConnected) {
currentText = 'Connected';
} else if (currentState === 'connecting') {
currentText = 'Connecting...';
} else if (currentState === 'disconnected') {
currentText = 'Disconnected';
} else if (currentState === 'disconnecting') {
currentText = 'Disconnecting...';
}
updateStatusIndicator(currentState, currentText);
return true;
}
return false;
}
if (!registerStatusCallback()) {
var checkInterval = setInterval(function() {
if (registerStatusCallback()) {
clearInterval(checkInterval);
}
}, 100);
setTimeout(function() {
clearInterval(checkInterval);
}, 5000);
}
}
function updateStatusIndicator(state, text) { function updateStatusIndicator(state, text) {
var statusDot = document.getElementById('statusDot'); var statusDot = document.getElementById('statusDot');
var statusText = document.getElementById('statusText'); var statusText = document.getElementById('statusText');
if (!statusDot || !statusText) { if (!statusDot || !statusText) {
return; return;
} }
// Remove all color classes
statusDot.classList.remove('bg-green-500', 'bg-yellow-500', 'bg-red-500', 'bg-gray-400'); statusDot.classList.remove('bg-green-500', 'bg-yellow-500', 'bg-red-500', 'bg-gray-400');
// Set color and text based on state
switch (state) { switch (state) {
case 'connected': case 'connected':
statusDot.classList.add('bg-green-500'); statusDot.classList.add('bg-green-500');
@@ -63,82 +99,30 @@ function updateStatusIndicator(state, text) {
} }
} }
// Initialize status indicator // =========================================================================
function initStatusIndicator() { // WebSocket Tooltip
// Set initial state // =========================================================================
updateStatusIndicator('connecting', 'Connecting...');
// Register callback with WebSocket manager when available
function registerStatusCallback() {
if (typeof wsManager !== 'undefined' && wsManager) {
// Register callback for future status changes
wsManager.onStatusChange(function(state, text) {
updateStatusIndicator(state, text);
});
// Immediately update status based on current connection state
// This handles the case where connection was established before callback registration
var currentState = wsManager.getConnectionState();
var currentText = 'Connecting...';
if (currentState === 'connected' && wsManager.isConnected) {
currentText = 'Connected';
} else if (currentState === 'connecting') {
currentText = 'Connecting...';
} else if (currentState === 'disconnected') {
currentText = 'Disconnected';
} else if (currentState === 'disconnecting') {
currentText = 'Disconnecting...';
}
updateStatusIndicator(currentState, currentText);
return true;
}
return false;
}
if (!registerStatusCallback()) {
// Wait for WebSocket manager to be available
var checkInterval = setInterval(function() {
if (registerStatusCallback()) {
clearInterval(checkInterval);
}
}, 100);
// Stop checking after 5 seconds to avoid infinite loop
setTimeout(function() {
clearInterval(checkInterval);
}, 5000);
}
}
// Create and manage WebSocket tooltip
function createWebSocketTooltip() { function createWebSocketTooltip() {
// Create tooltip element
const tooltip = document.createElement('div'); const tooltip = document.createElement('div');
tooltip.id = 'wsTooltip'; tooltip.id = 'wsTooltip';
tooltip.className = 'fixed z-50 px-3 py-2 bg-gray-900 text-white text-xs rounded shadow-lg pointer-events-none opacity-0 transition-opacity duration-200'; tooltip.className = 'fixed z-50 px-3 py-2 bg-gray-900 text-white text-xs rounded shadow-lg pointer-events-none opacity-0 transition-opacity duration-200';
tooltip.style.display = 'none'; tooltip.style.display = 'none';
tooltip.style.minWidth = '200px'; tooltip.style.minWidth = '200px';
document.body.appendChild(tooltip); document.body.appendChild(tooltip);
const statusEl = document.getElementById('backendStatus'); const statusEl = document.getElementById('backendStatus');
if (!statusEl) { if (!statusEl) {
return; return;
} }
let tooltipUpdateInterval = null; let tooltipUpdateInterval = null;
function updateTooltipContent() { function updateTooltipContent() {
if (!wsManager || !wsManager.isConnected) { if (!wsManager || !wsManager.isConnected) {
return; return;
} }
const info = wsManager.getConnectionInfo(); const info = wsManager.getConnectionInfo();
if (!info) { if (!info) {
return; return;
} }
tooltip.innerHTML = ` tooltip.innerHTML = `
<div class="font-semibold mb-2 text-green-400 border-b border-gray-700 pb-1">WebSocket Connection</div> <div class="font-semibold mb-2 text-green-400 border-b border-gray-700 pb-1">WebSocket Connection</div>
<div class="space-y-1"> <div class="space-y-1">
@@ -165,21 +149,15 @@ function createWebSocketTooltip() {
</div> </div>
`; `;
} }
function showTooltip(e) { function showTooltip(e) {
if (!wsManager || !wsManager.isConnected) { if (!wsManager || !wsManager.isConnected) {
return; return;
} }
updateTooltipContent(); updateTooltipContent();
const rect = statusEl.getBoundingClientRect(); const rect = statusEl.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect();
// Position tooltip below the status element, centered
let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2); let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
let top = rect.bottom + 8; let top = rect.bottom + 8;
// Adjust if tooltip would go off screen
if (left < 8) left = 8; if (left < 8) left = 8;
if (left + tooltipRect.width > window.innerWidth - 8) { if (left + tooltipRect.width > window.innerWidth - 8) {
left = window.innerWidth - tooltipRect.width - 8; left = window.innerWidth - tooltipRect.width - 8;
@@ -187,37 +165,29 @@ function createWebSocketTooltip() {
if (top + tooltipRect.height > window.innerHeight - 8) { if (top + tooltipRect.height > window.innerHeight - 8) {
top = rect.top - tooltipRect.height - 8; top = rect.top - tooltipRect.height - 8;
} }
tooltip.style.left = left + 'px'; tooltip.style.left = left + 'px';
tooltip.style.top = top + 'px'; tooltip.style.top = top + 'px';
tooltip.style.display = 'block'; tooltip.style.display = 'block';
setTimeout(() => { setTimeout(() => {
tooltip.style.opacity = '1'; tooltip.style.opacity = '1';
}, 10); }, 10);
// Update tooltip content every second while visible
if (tooltipUpdateInterval) { if (tooltipUpdateInterval) {
clearInterval(tooltipUpdateInterval); clearInterval(tooltipUpdateInterval);
} }
tooltipUpdateInterval = setInterval(updateTooltipContent, 1000); tooltipUpdateInterval = setInterval(updateTooltipContent, 1000);
} }
function hideTooltip() { function hideTooltip() {
tooltip.style.opacity = '0'; tooltip.style.opacity = '0';
setTimeout(() => { setTimeout(() => {
tooltip.style.display = 'none'; tooltip.style.display = 'none';
}, 200); }, 200);
if (tooltipUpdateInterval) { if (tooltipUpdateInterval) {
clearInterval(tooltipUpdateInterval); clearInterval(tooltipUpdateInterval);
tooltipUpdateInterval = null; tooltipUpdateInterval = null;
} }
} }
statusEl.addEventListener('mouseenter', showTooltip); statusEl.addEventListener('mouseenter', showTooltip);
statusEl.addEventListener('mouseleave', hideTooltip); statusEl.addEventListener('mouseleave', hideTooltip);
// Also hide tooltip when status changes to disconnected
if (typeof wsManager !== 'undefined' && wsManager) { if (typeof wsManager !== 'undefined' && wsManager) {
wsManager.onStatusChange(function(state, text) { wsManager.onStatusChange(function(state, text) {
if (state !== 'connected') { if (state !== 'connected') {
@@ -225,7 +195,6 @@ function createWebSocketTooltip() {
} }
}); });
} else { } else {
// Wait for WebSocket manager to be available
var checkInterval = setInterval(function() { var checkInterval = setInterval(function() {
if (typeof wsManager !== 'undefined' && wsManager) { if (typeof wsManager !== 'undefined' && wsManager) {
wsManager.onStatusChange(function(state, text) { wsManager.onStatusChange(function(state, text) {
@@ -239,14 +208,16 @@ function createWebSocketTooltip() {
} }
} }
// Initialize all header components // =========================================================================
// Initialization
// =========================================================================
function initHeader() { function initHeader() {
initClock(); initClock();
initStatusIndicator(); initStatusIndicator();
createWebSocketTooltip(); createWebSocketTooltip();
} }
// Cleanup on page unload
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', function() { window.addEventListener('beforeunload', function() {
if (clockInterval) { if (clockInterval) {

View File

@@ -1,6 +1,10 @@
// Ignore IPs tag management functions for Fail2ban UI // "Ignore IPs" tag management for Fail2ban UI
"use strict"; "use strict";
// =========================================================================
// Tag Rendering, Adding and Removing Functions
// =========================================================================
function renderIgnoreIPsTags(ips) { function renderIgnoreIPsTags(ips) {
const container = document.getElementById('ignoreIPsTags'); const container = document.getElementById('ignoreIPsTags');
if (!container) return; if (!container) return;
@@ -16,33 +20,25 @@ function renderIgnoreIPsTags(ips) {
function addIgnoreIPTag(ip) { function addIgnoreIPTag(ip) {
if (!ip || !ip.trim()) return; if (!ip || !ip.trim()) return;
const trimmedIP = ip.trim(); const trimmedIP = ip.trim();
// Validate IP before adding - isValidIP is in validation.js
if (typeof isValidIP === 'function' && !isValidIP(trimmedIP)) { if (typeof isValidIP === 'function' && !isValidIP(trimmedIP)) {
if (typeof showToast === 'function') { if (typeof showToast === 'function') {
showToast('Invalid IP address, CIDR, or hostname: ' + trimmedIP, 'error'); showToast('Invalid IP address, CIDR, or hostname: ' + trimmedIP, 'error');
} }
return; return;
} }
const container = document.getElementById('ignoreIPsTags'); const container = document.getElementById('ignoreIPsTags');
if (!container) return; if (!container) return;
const existingTags = Array.from(container.querySelectorAll('.ignore-ip-tag')).map(tag => tag.dataset.ip); const existingTags = Array.from(container.querySelectorAll('.ignore-ip-tag')).map(tag => tag.dataset.ip);
if (existingTags.includes(trimmedIP)) { if (existingTags.includes(trimmedIP)) {
return; // Already exists return;
} }
const tag = document.createElement('span'); const tag = document.createElement('span');
tag.className = 'ignore-ip-tag inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800'; tag.className = 'ignore-ip-tag inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800';
tag.dataset.ip = trimmedIP; tag.dataset.ip = trimmedIP;
const escapedIP = escapeHtml(trimmedIP); const escapedIP = escapeHtml(trimmedIP);
tag.innerHTML = escapedIP + ' <button type="button" class="ml-1 text-blue-600 hover:text-blue-800 focus:outline-none" onclick="removeIgnoreIPTag(\'' + escapedIP.replace(/'/g, "\\'") + '\')">×</button>'; tag.innerHTML = escapedIP + ' <button type="button" class="ml-1 text-blue-600 hover:text-blue-800 focus:outline-none" onclick="removeIgnoreIPTag(\'' + escapedIP.replace(/'/g, "\\'") + '\')">×</button>';
container.appendChild(tag); container.appendChild(tag);
// Clear input
const input = document.getElementById('ignoreIPInput'); const input = document.getElementById('ignoreIPInput');
if (input) input.value = ''; if (input) input.value = '';
} }
@@ -57,43 +53,32 @@ function removeIgnoreIPTag(ip) {
} }
} }
function getIgnoreIPsArray() { // =========================================================================
const container = document.getElementById('ignoreIPsTags'); // Input Handling
if (!container) return []; // =========================================================================
const tags = container.querySelectorAll('.ignore-ip-tag');
return Array.from(tags).map(tag => tag.dataset.ip).filter(ip => ip && ip.trim());
}
function setupIgnoreIPsInput() { function setupIgnoreIPsInput() {
const input = document.getElementById('ignoreIPInput'); const input = document.getElementById('ignoreIPInput');
if (!input) return; if (!input) return;
// Allow spaces for space-separated lists, but validate on paste/enter/blur
let lastValue = ''; let lastValue = '';
input.addEventListener('input', function(e) { input.addEventListener('input', function(e) {
// Allow spaces for space-separated lists
// Allow: 0-9, a-z, A-Z, :, ., /, -, _, space (for space-separated lists)
let value = this.value; let value = this.value;
// Remove any characters that aren't valid for IPs/hostnames or spaces
const filtered = value.replace(/[^0-9a-zA-Z:.\/\-\_\s]/g, ''); const filtered = value.replace(/[^0-9a-zA-Z:.\/\-\_\s]/g, '');
if (value !== filtered) { if (value !== filtered) {
this.value = filtered; this.value = filtered;
} }
lastValue = filtered; lastValue = filtered;
}); });
input.addEventListener('keydown', function(e) { input.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ',') { if (e.key === 'Enter' || e.key === ',') {
e.preventDefault(); e.preventDefault();
const value = input.value.trim(); const value = input.value.trim();
if (value) { if (value) {
// Support space or comma separated IPs
const ips = value.split(/[,\s]+/).filter(ip => ip.trim()); const ips = value.split(/[,\s]+/).filter(ip => ip.trim());
ips.forEach(ip => addIgnoreIPTag(ip.trim())); ips.forEach(ip => addIgnoreIPTag(ip.trim()));
} }
} }
}); });
input.addEventListener('blur', function(e) { input.addEventListener('blur', function(e) {
const value = input.value.trim(); const value = input.value.trim();
if (value) { if (value) {
@@ -101,21 +86,27 @@ function setupIgnoreIPsInput() {
ips.forEach(ip => addIgnoreIPTag(ip.trim())); ips.forEach(ip => addIgnoreIPTag(ip.trim()));
} }
}); });
// Handle paste events to automatically process space-separated lists
input.addEventListener('paste', function(e) { input.addEventListener('paste', function(e) {
// Allow default paste behavior first
setTimeout(() => { setTimeout(() => {
const value = input.value.trim(); const value = input.value.trim();
if (value) { if (value) {
// Check if pasted content contains spaces (likely a space-separated list)
if (value.includes(' ') || value.includes(',')) { if (value.includes(' ') || value.includes(',')) {
const ips = value.split(/[,\s]+/).filter(ip => ip.trim()); const ips = value.split(/[,\s]+/).filter(ip => ip.trim());
ips.forEach(ip => addIgnoreIPTag(ip.trim())); ips.forEach(ip => addIgnoreIPTag(ip.trim()));
input.value = ''; // Clear input after processing input.value = '';
} }
} }
}, 0); }, 0);
}); });
} }
// =========================================================================
// Data Access
// =========================================================================
function getIgnoreIPsArray() {
const container = document.getElementById('ignoreIPsTags');
if (!container) return [];
const tags = container.querySelectorAll('.ignore-ip-tag');
return Array.from(tags).map(tag => tag.dataset.ip).filter(ip => ip && ip.trim());
}

View File

@@ -1,37 +1,36 @@
// Initialization code for Fail2ban UI // App bootstrap and initialization.
"use strict"; "use strict";
// =========================================================================
// Bootstrap
// =========================================================================
window.addEventListener('DOMContentLoaded', function() { window.addEventListener('DOMContentLoaded', function() {
showLoading(true); showLoading(true);
// Check authentication status first (if auth.js is loaded)
if (typeof checkAuthStatus === 'function') { if (typeof checkAuthStatus === 'function') {
checkAuthStatus().then(function(authStatus) { checkAuthStatus().then(function(authStatus) {
// Only proceed with initialization if authenticated or OIDC disabled
if (!authStatus.enabled || authStatus.authenticated) { if (!authStatus.enabled || authStatus.authenticated) {
initializeApp(); initializeApp();
} else { } else {
// Not authenticated, login page will be shown by checkAuthStatus
showLoading(false); showLoading(false);
} }
}).catch(function(err) { }).catch(function(err) {
console.error('Auth check failed:', err); console.error('Auth check failed:', err);
// Proceed with initialization anyway (fallback)
initializeApp(); initializeApp();
}); });
} else { } else {
// Auth.js not loaded, proceed normally
initializeApp(); initializeApp();
} }
}); });
// =========================================================================
// App Initialization
// =========================================================================
function initializeApp() { function initializeApp() {
// Only display external IP if the element exists (not disabled via template variable)
if (document.getElementById('external-ip')) { if (document.getElementById('external-ip')) {
displayExternalIP(); displayExternalIP();
} }
// Initialize header components (clock and status indicator)
if (typeof initHeader === 'function') { if (typeof initHeader === 'function') {
initHeader(); initHeader();
} }
@@ -50,20 +49,16 @@ function initializeApp() {
} }
if (!registerBanEventHandler()) { if (!registerBanEventHandler()) {
// Wait for WebSocket manager to be available
var wsCheckInterval = setInterval(function() { var wsCheckInterval = setInterval(function() {
if (registerBanEventHandler()) { if (registerBanEventHandler()) {
clearInterval(wsCheckInterval); clearInterval(wsCheckInterval);
} }
}, 100); }, 100);
// Stop checking after 5 seconds
setTimeout(function() { setTimeout(function() {
clearInterval(wsCheckInterval); clearInterval(wsCheckInterval);
}, 5000); }, 5000);
} }
// Check LOTR mode on page load to apply immediately
fetch('/api/settings') fetch('/api/settings')
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
@@ -71,7 +66,6 @@ function initializeApp() {
if (typeof checkAndApplyLOTRTheme === 'function') { if (typeof checkAndApplyLOTRTheme === 'function') {
checkAndApplyLOTRTheme(alertCountries); checkAndApplyLOTRTheme(alertCountries);
} }
// Store in global for later use
if (typeof currentSettings === 'undefined') { if (typeof currentSettings === 'undefined') {
window.currentSettings = {}; window.currentSettings = {};
} }
@@ -81,7 +75,7 @@ function initializeApp() {
console.warn('Could not check LOTR on load:', err); console.warn('Could not check LOTR on load:', err);
}); });
// Version and update check: only on page load; UPDATE_CHECK=false disables external GitHub request // Check for updates and display version badge in the footer
var versionContainer = document.getElementById('version-badge-container'); var versionContainer = document.getElementById('version-badge-container');
if (versionContainer && versionContainer.getAttribute('data-update-check') === 'true') { if (versionContainer && versionContainer.getAttribute('data-update-check') === 'true') {
fetch('/api/version') fetch('/api/version')
@@ -98,9 +92,10 @@ function initializeApp() {
versionContainer.innerHTML = '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800" title="' + latestLabel + '">' + latestLabel + '</span>'; versionContainer.innerHTML = '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800" title="' + latestLabel + '">' + latestLabel + '</span>';
} }
}) })
.catch(function() { /* ignore; no badge on error */ }); .catch(function() { });
} }
// Load servers and translations, then render the dashboard and initialize tooltips and search
Promise.all([ Promise.all([
loadServers(), loadServers(),
getTranslationsSettingsOnPageload() getTranslationsSettingsOnPageload()
@@ -119,12 +114,12 @@ function initializeApp() {
} }
}) })
.finally(function() { .finally(function() {
initializeTooltips(); // Initialize tooltips after fetching and rendering initializeTooltips();
initializeSearch(); initializeSearch();
showLoading(false); showLoading(false);
}); });
// Setup Select2 for alert countries // jQuery-dependent setup (Select2 for alert countries)
$(document).ready(function() { $(document).ready(function() {
$('#alertCountries').select2({ $('#alertCountries').select2({
placeholder: 'Select countries..', placeholder: 'Select countries..',
@@ -132,6 +127,7 @@ function initializeApp() {
width: '100%' width: '100%'
}); });
// When "ALL" is selected, deselect other countries and vice versa
$('#alertCountries').on('select2:select', function(e) { $('#alertCountries').on('select2:select', function(e) {
var selectedValue = e.params.data.id; var selectedValue = e.params.data.id;
var currentValues = $('#alertCountries').val() || []; var currentValues = $('#alertCountries').val() || [];
@@ -147,7 +143,6 @@ function initializeApp() {
$('#alertCountries').val(newValues).trigger('change'); $('#alertCountries').val(newValues).trigger('change');
} }
} }
// Check LOTR mode after selection change
setTimeout(function() { setTimeout(function() {
const selectedCountries = $('#alertCountries').val() || []; const selectedCountries = $('#alertCountries').val() || [];
if (typeof checkAndApplyLOTRTheme === 'function') { if (typeof checkAndApplyLOTRTheme === 'function') {
@@ -157,7 +152,6 @@ function initializeApp() {
}); });
$('#alertCountries').on('select2:unselect', function(e) { $('#alertCountries').on('select2:unselect', function(e) {
// Check LOTR mode after deselection
setTimeout(function() { setTimeout(function() {
const selectedCountries = $('#alertCountries').val() || []; const selectedCountries = $('#alertCountries').val() || [];
if (typeof checkAndApplyLOTRTheme === 'function') { if (typeof checkAndApplyLOTRTheme === 'function') {
@@ -175,17 +169,14 @@ function initializeApp() {
}); });
} }
// Setup IgnoreIPs tag input
if (typeof setupIgnoreIPsInput === 'function') { if (typeof setupIgnoreIPsInput === 'function') {
setupIgnoreIPsInput(); setupIgnoreIPsInput();
} }
// Setup form validation
if (typeof setupFormValidation === 'function') { if (typeof setupFormValidation === 'function') {
setupFormValidation(); setupFormValidation();
} }
// Setup advanced integration fields
const advancedIntegrationSelect = document.getElementById('advancedIntegrationSelect'); const advancedIntegrationSelect = document.getElementById('advancedIntegrationSelect');
if (advancedIntegrationSelect && typeof updateAdvancedIntegrationFields === 'function') { if (advancedIntegrationSelect && typeof updateAdvancedIntegrationFields === 'function') {
advancedIntegrationSelect.addEventListener('change', updateAdvancedIntegrationFields); advancedIntegrationSelect.addEventListener('change', updateAdvancedIntegrationFields);

View File

@@ -8,31 +8,28 @@ function isLOTRMode(alertCountries) {
return alertCountries.includes('LOTR'); return alertCountries.includes('LOTR');
} }
// =========================================================================
// Theme Application
// =========================================================================
function applyLOTRTheme(active) { function applyLOTRTheme(active) {
const body = document.body; const body = document.body;
const lotrCSS = document.getElementById('lotr-css'); const lotrCSS = document.getElementById('lotr-css');
if (active) { if (active) {
// Enable CSS first
if (lotrCSS) { if (lotrCSS) {
lotrCSS.disabled = false; lotrCSS.disabled = false;
} }
// Then add class to body
body.classList.add('lotr-mode'); body.classList.add('lotr-mode');
isLOTRModeActive = true; isLOTRModeActive = true;
console.log('🎭 LOTR Mode Activated - Welcome to Middle-earth!'); console.log('🎭 LOTR Mode Activated - Welcome to Middle-earth!');
} else { } else {
// Remove class first
body.classList.remove('lotr-mode'); body.classList.remove('lotr-mode');
// Then disable CSS
if (lotrCSS) { if (lotrCSS) {
lotrCSS.disabled = true; lotrCSS.disabled = true;
} }
isLOTRModeActive = false; isLOTRModeActive = false;
console.log('🎭 LOTR Mode Deactivated'); console.log('🎭 LOTR Mode Deactivated');
} }
// Force a reflow to ensure styles are applied
void body.offsetHeight; void body.offsetHeight;
} }
@@ -46,51 +43,36 @@ function checkAndApplyLOTRTheme(alertCountries) {
function updateLOTRTerminology(active) { function updateLOTRTerminology(active) {
if (active) { if (active) {
// Update navigation title
const navTitle = document.querySelector('nav .text-xl'); const navTitle = document.querySelector('nav .text-xl');
if (navTitle) { if (navTitle) {
navTitle.textContent = 'Middle-earth Security'; navTitle.textContent = 'Middle-earth Security';
} }
// Update page title
const pageTitle = document.querySelector('title'); const pageTitle = document.querySelector('title');
if (pageTitle) { if (pageTitle) {
pageTitle.textContent = 'Middle-earth Security Realm'; pageTitle.textContent = 'Middle-earth Security Realm';
} }
// Update dashboard terminology
updateDashboardLOTRTerminology(true); updateDashboardLOTRTerminology(true);
// Add decorative elements
addLOTRDecorations(); addLOTRDecorations();
} else { } else {
// Restore original text
const navTitle = document.querySelector('nav .text-xl'); const navTitle = document.querySelector('nav .text-xl');
if (navTitle) { if (navTitle) {
navTitle.textContent = 'Fail2ban UI'; navTitle.textContent = 'Fail2ban UI';
} }
const pageTitle = document.querySelector('title'); const pageTitle = document.querySelector('title');
if (pageTitle && pageTitle.hasAttribute('data-i18n')) { if (pageTitle && pageTitle.hasAttribute('data-i18n')) {
const i18nKey = pageTitle.getAttribute('data-i18n'); const i18nKey = pageTitle.getAttribute('data-i18n');
pageTitle.textContent = t(i18nKey, 'Fail2ban UI Dashboard'); pageTitle.textContent = t(i18nKey, 'Fail2ban UI Dashboard');
} }
// Restore dashboard terminology
updateDashboardLOTRTerminology(false); updateDashboardLOTRTerminology(false);
// Remove decorative elements
removeLOTRDecorations(); removeLOTRDecorations();
} }
} }
function updateDashboardLOTRTerminology(active) { function updateDashboardLOTRTerminology(active) {
// Update text elements that use data-i18n
const elements = document.querySelectorAll('[data-i18n]'); const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(el => { elements.forEach(el => {
const i18nKey = el.getAttribute('data-i18n'); const i18nKey = el.getAttribute('data-i18n');
if (active) { if (active) {
// Check for LOTR-specific translations
if (i18nKey === 'dashboard.cards.total_banned') { if (i18nKey === 'dashboard.cards.total_banned') {
el.textContent = t('lotr.threats_banished', 'Threats Banished'); el.textContent = t('lotr.threats_banished', 'Threats Banished');
} else if (i18nKey === 'dashboard.table.banned_ips') { } else if (i18nKey === 'dashboard.table.banned_ips') {
@@ -101,14 +83,11 @@ function updateDashboardLOTRTerminology(active) {
el.textContent = t('lotr.realms_protected', 'Manage Realms'); el.textContent = t('lotr.realms_protected', 'Manage Realms');
} }
} else { } else {
// Restore original translations
if (i18nKey) { if (i18nKey) {
el.textContent = t(i18nKey, el.textContent); el.textContent = t(i18nKey, el.textContent);
} }
} }
}); });
// Update "Unban" buttons
const unbanButtons = document.querySelectorAll('button, a'); const unbanButtons = document.querySelectorAll('button, a');
unbanButtons.forEach(btn => { unbanButtons.forEach(btn => {
if (btn.textContent && btn.textContent.includes('Unban')) { if (btn.textContent && btn.textContent.includes('Unban')) {
@@ -122,26 +101,20 @@ function updateDashboardLOTRTerminology(active) {
} }
function addLOTRDecorations() { function addLOTRDecorations() {
// Add decorative divider to settings section if not already present
const settingsSection = document.getElementById('settingsSection'); const settingsSection = document.getElementById('settingsSection');
if (settingsSection && !settingsSection.querySelector('.lotr-divider')) { if (settingsSection && !settingsSection.querySelector('.lotr-divider')) {
const divider = document.createElement('div'); const divider = document.createElement('div');
divider.className = 'lotr-divider'; divider.className = 'lotr-divider';
divider.style.marginTop = '20px'; divider.style.marginTop = '20px';
divider.style.marginBottom = '20px'; divider.style.marginBottom = '20px';
// Find the first child element (not text node) to insert before
const firstChild = Array.from(settingsSection.childNodes).find( const firstChild = Array.from(settingsSection.childNodes).find(
node => node.nodeType === Node.ELEMENT_NODE node => node.nodeType === Node.ELEMENT_NODE
); );
if (firstChild && firstChild.parentNode === settingsSection) { if (firstChild && firstChild.parentNode === settingsSection) {
settingsSection.insertBefore(divider, firstChild); settingsSection.insertBefore(divider, firstChild);
} else if (settingsSection.firstChild) { } else if (settingsSection.firstChild) {
// Fallback: append if insertBefore fails
settingsSection.insertBefore(divider, settingsSection.firstChild); settingsSection.insertBefore(divider, settingsSection.firstChild);
} else { } else {
// Last resort: append to end
settingsSection.appendChild(divider); settingsSection.appendChild(divider);
} }
} }
@@ -151,4 +124,3 @@ function removeLOTRDecorations() {
const dividers = document.querySelectorAll('.lotr-divider'); const dividers = document.querySelectorAll('.lotr-divider');
dividers.forEach(div => div.remove()); dividers.forEach(div => div.remove());
} }

View File

@@ -1,6 +1,10 @@
// Modal management functions for Fail2ban UI // Modal management for Fail2ban UI
"use strict"; "use strict";
// =========================================================================
// Modal Lifecycle
// =========================================================================
function updateBodyScrollLock() { function updateBodyScrollLock() {
if (openModalCount > 0) { if (openModalCount > 0) {
document.body.classList.add('modal-open'); document.body.classList.add('modal-open');
@@ -9,7 +13,6 @@ function updateBodyScrollLock() {
} }
} }
// Close modal
function closeModal(modalId) { function closeModal(modalId) {
var modal = document.getElementById(modalId); var modal = document.getElementById(modalId);
if (!modal || modal.classList.contains('hidden')) { if (!modal || modal.classList.contains('hidden')) {
@@ -20,7 +23,6 @@ function closeModal(modalId) {
updateBodyScrollLock(); updateBodyScrollLock();
} }
// Open modal
function openModal(modalId) { function openModal(modalId) {
var modal = document.getElementById(modalId); var modal = document.getElementById(modalId);
if (!modal || !modal.classList.contains('hidden')) { if (!modal || !modal.classList.contains('hidden')) {
@@ -32,6 +34,11 @@ function openModal(modalId) {
updateBodyScrollLock(); updateBodyScrollLock();
} }
// =========================================================================
// Whois and Logs Modal
// =========================================================================
// Whois modal
function openWhoisModal(eventIndex) { function openWhoisModal(eventIndex) {
if (!latestBanEvents || !latestBanEvents[eventIndex]) { if (!latestBanEvents || !latestBanEvents[eventIndex]) {
showToast("Event not found", 'error'); showToast("Event not found", 'error');
@@ -42,13 +49,13 @@ function openWhoisModal(eventIndex) {
showToast("No whois data available for this event", 'info'); showToast("No whois data available for this event", 'info');
return; return;
} }
document.getElementById('whoisModalIP').textContent = event.ip || 'N/A'; document.getElementById('whoisModalIP').textContent = event.ip || 'N/A';
var contentEl = document.getElementById('whoisModalContent'); var contentEl = document.getElementById('whoisModalContent');
contentEl.textContent = event.whois; contentEl.textContent = event.whois;
openModal('whoisModal'); openModal('whoisModal');
} }
// Logs modal
function openLogsModal(eventIndex) { function openLogsModal(eventIndex) {
if (!latestBanEvents || !latestBanEvents[eventIndex]) { if (!latestBanEvents || !latestBanEvents[eventIndex]) {
showToast("Event not found", 'error'); showToast("Event not found", 'error');
@@ -59,27 +66,21 @@ function openLogsModal(eventIndex) {
showToast("No logs data available for this event", 'info'); showToast("No logs data available for this event", 'info');
return; return;
} }
document.getElementById('logsModalIP').textContent = event.ip || 'N/A'; document.getElementById('logsModalIP').textContent = event.ip || 'N/A';
document.getElementById('logsModalJail').textContent = event.jail || 'N/A'; document.getElementById('logsModalJail').textContent = event.jail || 'N/A';
var logs = event.logs; var logs = event.logs;
var ip = event.ip || ''; var ip = event.ip || '';
var logLines = logs.split('\n'); var logLines = logs.split('\n');
// Determine which lines are suspicious (bad requests)
var suspiciousIndices = []; var suspiciousIndices = [];
for (var i = 0; i < logLines.length; i++) { for (var i = 0; i < logLines.length; i++) {
if (isSuspiciousLogLine(logLines[i], ip)) { if (isSuspiciousLogLine(logLines[i], ip)) {
suspiciousIndices.push(i); suspiciousIndices.push(i);
} }
} }
var contentEl = document.getElementById('logsModalContent'); var contentEl = document.getElementById('logsModalContent');
if (suspiciousIndices.length) { if (suspiciousIndices.length) {
var highlightMap = {}; var highlightMap = {};
suspiciousIndices.forEach(function(idx) { highlightMap[idx] = true; }); suspiciousIndices.forEach(function(idx) { highlightMap[idx] = true; });
var html = ''; var html = '';
for (var j = 0; j < logLines.length; j++) { for (var j = 0; j < logLines.length; j++) {
var safeLine = escapeHtml(logLines[j] || ''); var safeLine = escapeHtml(logLines[j] || '');
@@ -91,57 +92,14 @@ function openLogsModal(eventIndex) {
} }
contentEl.innerHTML = html; contentEl.innerHTML = html;
} else { } else {
// No suspicious lines detected; show raw logs without highlighting
contentEl.textContent = logs; contentEl.textContent = logs;
} }
openModal('logsModal'); openModal('logsModal');
} }
function isSuspiciousLogLine(line, ip) { // =========================================================================
if (!line) { // Ban Insights Modal
return false; // =========================================================================
}
var containsIP = ip && line.indexOf(ip) !== -1;
var lowered = line.toLowerCase();
// Detect HTTP status codes (>= 300 considered problematic)
var statusMatch = line.match(/"[^"]*"\s+(\d{3})\b/);
if (!statusMatch) {
statusMatch = line.match(/\s(\d{3})\s+(?:\d+|-)/);
}
var statusCode = statusMatch ? parseInt(statusMatch[1], 10) : NaN;
var hasBadStatus = !isNaN(statusCode) && statusCode >= 300;
// Detect common attack indicators in URLs/payloads
var indicators = [
'../',
'%2e%2e',
'%252e%252e',
'%24%7b',
'${',
'/etc/passwd',
'select%20',
'union%20',
'cmd=',
'wget',
'curl ',
'nslookup',
'/xmlrpc.php',
'/wp-admin',
'/cgi-bin',
'content-length: 0'
];
var hasIndicator = indicators.some(function(ind) {
return lowered.indexOf(ind) !== -1;
});
if (containsIP) {
return hasBadStatus || hasIndicator;
}
return (hasBadStatus || hasIndicator) && !ip;
}
function openBanInsightsModal() { function openBanInsightsModal() {
var countriesContainer = document.getElementById('countryStatsContainer'); var countriesContainer = document.getElementById('countryStatsContainer');
@@ -176,7 +134,6 @@ function openBanInsightsModal() {
+ '</div>'; + '</div>';
}).join(''); }).join('');
} }
var countries = (latestBanInsights && latestBanInsights.countries) || []; var countries = (latestBanInsights && latestBanInsights.countries) || [];
if (!countries.length) { if (!countries.length) {
countriesContainer.innerHTML = '<p class="text-sm text-gray-500" data-i18n="logs.modal.insights_countries_empty">No bans recorded for this period.</p>'; countriesContainer.innerHTML = '<p class="text-sm text-gray-500" data-i18n="logs.modal.insights_countries_empty">No bans recorded for this period.</p>';
@@ -201,7 +158,6 @@ function openBanInsightsModal() {
}).join(''); }).join('');
countriesContainer.innerHTML = countryHTML; countriesContainer.innerHTML = countryHTML;
} }
var recurring = (latestBanInsights && latestBanInsights.recurring) || []; var recurring = (latestBanInsights && latestBanInsights.recurring) || [];
if (!recurring.length) { if (!recurring.length) {
recurringContainer.innerHTML = '<p class="text-sm text-gray-500" data-i18n="logs.modal.insights_recurring_empty">No recurring IPs detected.</p>'; recurringContainer.innerHTML = '<p class="text-sm text-gray-500" data-i18n="logs.modal.insights_recurring_empty">No recurring IPs detected.</p>';
@@ -226,10 +182,8 @@ function openBanInsightsModal() {
}).join(''); }).join('');
recurringContainer.innerHTML = recurringHTML; recurringContainer.innerHTML = recurringHTML;
} }
if (typeof updateTranslations === 'function') { if (typeof updateTranslations === 'function') {
updateTranslations(); updateTranslations();
} }
openModal('banInsightsModal'); openModal('banInsightsModal');
} }

View File

@@ -1,7 +1,10 @@
// Translation functions for Fail2ban UI // Translation implementation for Fail2ban UI.
"use strict"; "use strict";
// Loads translation JSON file for given language (e.g., en, de, etc.) // =========================================================================
// Translation Engine
// =========================================================================
function loadTranslations(lang) { function loadTranslations(lang) {
$.getJSON('/locales/' + lang + '.json') $.getJSON('/locales/' + lang + '.json')
.done(function(data) { .done(function(data) {
@@ -13,7 +16,6 @@ function loadTranslations(lang) {
}); });
} }
// Updates all elements with data-i18n attribute with corresponding translation.
function updateTranslations() { function updateTranslations() {
$('[data-i18n]').each(function() { $('[data-i18n]').each(function() {
var key = $(this).data('i18n'); var key = $(this).data('i18n');
@@ -21,7 +23,6 @@ function updateTranslations() {
$(this).text(translations[key]); $(this).text(translations[key]);
} }
}); });
// Updates placeholders.
$('[data-i18n-placeholder]').each(function() { $('[data-i18n-placeholder]').each(function() {
var key = $(this).data('i18n-placeholder'); var key = $(this).data('i18n-placeholder');
if (translations[key]) { if (translations[key]) {
@@ -43,4 +44,3 @@ function getTranslationsSettingsOnPageload() {
loadTranslations('en'); loadTranslations('en');
}); });
} }

View File

@@ -1,6 +1,10 @@
// Utility functions for Fail2ban UI // Shared utilities for Fail2ban UI.
"use strict"; "use strict";
// =========================================================================
// Data Normalization
// =========================================================================
function normalizeInsights(data) { function normalizeInsights(data) {
var normalized = data && typeof data === 'object' ? data : {}; var normalized = data && typeof data === 'object' ? data : {};
if (!normalized.totals || typeof normalized.totals !== 'object') { if (!normalized.totals || typeof normalized.totals !== 'object') {
@@ -26,6 +30,10 @@ function t(key, fallback) {
return fallback !== undefined ? fallback : key; return fallback !== undefined ? fallback : key;
} }
// =========================================================================
// Focus Management
// =========================================================================
function captureFocusState(container) { function captureFocusState(container) {
var active = document.activeElement; var active = document.activeElement;
if (!active || !container || !container.contains(active)) { if (!active || !container || !container.contains(active)) {
@@ -40,9 +48,7 @@ function captureFocusState(container) {
state.selectionStart = active.selectionStart; state.selectionStart = active.selectionStart;
state.selectionEnd = active.selectionEnd; state.selectionEnd = active.selectionEnd;
} }
} catch (err) { } catch (err) {}
// Ignore selection errors for elements that do not support it.
}
return state; return state;
} }
@@ -65,11 +71,13 @@ function restoreFocusState(state) {
if (typeof state.selectionStart === 'number' && typeof state.selectionEnd === 'number' && typeof next.setSelectionRange === 'function') { if (typeof state.selectionStart === 'number' && typeof state.selectionEnd === 'number' && typeof next.setSelectionRange === 'function') {
next.setSelectionRange(state.selectionStart, state.selectionEnd); next.setSelectionRange(state.selectionStart, state.selectionEnd);
} }
} catch (err) { } catch (err) {}
// Element may not support setSelectionRange; ignore.
}
} }
// =========================================================================
// String Helpers
// =========================================================================
function highlightQueryMatch(value, query) { function highlightQueryMatch(value, query) {
var text = value || ''; var text = value || '';
if (!query) { if (!query) {
@@ -104,3 +112,46 @@ function slugifyId(value, prefix) {
return (prefix || 'id') + '-' + base + '-' + hash; return (prefix || 'id') + '-' + base + '-' + hash;
} }
// =========================================================================
// Log Analysis Helper
// =========================================================================
function isSuspiciousLogLine(line, ip) {
if (!line) {
return false;
}
var containsIP = ip && line.indexOf(ip) !== -1;
var lowered = line.toLowerCase();
// Detect HTTP status codes (>= 300 considered problematic)
var statusMatch = line.match(/"[^"]*"\s+(\d{3})\b/);
if (!statusMatch) {
statusMatch = line.match(/\s(\d{3})\s+(?:\d+|-)/);
}
var statusCode = statusMatch ? parseInt(statusMatch[1], 10) : NaN;
var hasBadStatus = !isNaN(statusCode) && statusCode >= 300;
// Detect common attack indicators in URLs/payloads
var indicators = [
'../',
'%2e%2e',
'%252e%252e',
'%24%7b',
'${',
'/etc/passwd',
'select%20',
'union%20',
'cmd=',
'wget',
'curl ',
'nslookup',
'/xmlrpc.php',
'/wp-admin',
'/cgi-bin',
'content-length: 0'
];
var hasIndicator = indicators.some(function(ind) {
return lowered.indexOf(ind) !== -1;
}); if (containsIP) {
return hasBadStatus || hasIndicator;
}
return (hasBadStatus || hasIndicator) && !ip;
}