Implement sections to core.js and websocket.js

This commit is contained in:
2026-02-16 10:08:33 +01:00
parent d1a13da8b0
commit 4962d398a1
2 changed files with 47 additions and 46 deletions

View File

@@ -1,6 +1,10 @@
// Core utility functions for Fail2ban UI // Core UI utilities: loading overlay, toasts, formatting, search, and navigation.
"use strict";
// =========================================================================
// Loading Overlay
// =========================================================================
// Toggle the loading overlay (with !important)
function showLoading(show) { function showLoading(show) {
var overlay = document.getElementById('loading-overlay'); var overlay = document.getElementById('loading-overlay');
if (overlay) { if (overlay) {
@@ -8,18 +12,21 @@ function showLoading(show) {
overlay.style.setProperty('display', 'flex', 'important'); overlay.style.setProperty('display', 'flex', 'important');
setTimeout(() => overlay.classList.add('show'), 10); setTimeout(() => overlay.classList.add('show'), 10);
} else { } else {
overlay.classList.remove('show'); // Start fade-out overlay.classList.remove('show');
setTimeout(() => overlay.style.setProperty('display', 'none', 'important'), 400); setTimeout(() => overlay.style.setProperty('display', 'none', 'important'), 400);
} }
} }
} }
// Show toast notification // =========================================================================
// Toast Notifications
// =========================================================================
function showToast(message, type, duration) { function showToast(message, type, duration) {
var container = document.getElementById('toast-container'); var container = document.getElementById('toast-container');
if (!container || !message) return; if (!container || !message) return;
// Handle ban event objects // Show ban/unban events as their own styled toast
if (typeof message === 'object' && message.type === 'ban_event') { if (typeof message === 'object' && message.type === 'ban_event') {
showBanEventToast(message.data || message); showBanEventToast(message.data || message);
return; return;
@@ -29,7 +36,6 @@ function showToast(message, type, duration) {
var variant = type || 'info'; var variant = type || 'info';
toast.className = 'toast toast-' + variant; toast.className = 'toast toast-' + variant;
// Build inner layout with close button
var wrapper = document.createElement('div'); var wrapper = document.createElement('div');
wrapper.className = 'flex items-start'; wrapper.className = 'flex items-start';
@@ -46,7 +52,6 @@ function showToast(message, type, duration) {
wrapper.appendChild(closeBtn); wrapper.appendChild(closeBtn);
toast.appendChild(wrapper); toast.appendChild(wrapper);
// Close button handler
closeBtn.addEventListener('click', function(e) { closeBtn.addEventListener('click', function(e) {
e.stopPropagation(); e.stopPropagation();
clearTimeout(autoRemoveTimer); clearTimeout(autoRemoveTimer);
@@ -66,7 +71,7 @@ function showToast(message, type, duration) {
}, duration || 5000); }, duration || 5000);
} }
// Show toast for ban event // One function for both ban and unban events
function showBanEventToast(event) { function showBanEventToast(event) {
var container = document.getElementById('toast-container'); var container = document.getElementById('toast-container');
if (!container || !event) return; if (!container || !event) return;
@@ -105,7 +110,6 @@ function showBanEventToast(event) {
+ ' </button>' + ' </button>'
+ '</div>'; + '</div>';
// Close button handler
var closeBtn = toast.querySelector('button'); var closeBtn = toast.querySelector('button');
closeBtn.addEventListener('click', function(e) { closeBtn.addEventListener('click', function(e) {
e.stopPropagation(); e.stopPropagation();
@@ -137,7 +141,11 @@ function showBanEventToast(event) {
}, 5000); }, 5000);
} }
// Escape HTML to prevent XSS // =========================================================================
// Formatting Helpers
// =========================================================================
// Escape HTML special characters to prevent XSS
function escapeHtml(value) { function escapeHtml(value) {
if (value === undefined || value === null) return ''; if (value === undefined || value === null) return '';
return String(value).replace(/[&<>"']/g, function(match) { return String(value).replace(/[&<>"']/g, function(match) {
@@ -151,7 +159,7 @@ function escapeHtml(value) {
}); });
} }
// Format number with locale // Format numbers in a human-readable way (1,000,000)
function formatNumber(value) { function formatNumber(value) {
var num = Number(value); var num = Number(value);
if (!isFinite(num)) { if (!isFinite(num)) {
@@ -164,14 +172,13 @@ function formatNumber(value) {
} }
} }
// Format date/time (custom format for dashboard) // Format date and time in a human-readable way (YYYY.MM.DD, HH:MM:SS)
function formatDateTime(value) { function formatDateTime(value) {
if (!value) return ''; if (!value) return '';
var date = new Date(value); var date = new Date(value);
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
return value; return value;
} }
// Format as "2025.11.12, 21:21:52"
var year = date.getFullYear(); var year = date.getFullYear();
var month = String(date.getMonth() + 1).padStart(2, '0'); var month = String(date.getMonth() + 1).padStart(2, '0');
var day = String(date.getDate()).padStart(2, '0'); var day = String(date.getDate()).padStart(2, '0');
@@ -181,7 +188,11 @@ function formatDateTime(value) {
return year + '.' + month + '.' + day + ', ' + hours + ':' + minutes + ':' + seconds; return year + '.' + month + '.' + day + ', ' + hours + ':' + minutes + ':' + seconds;
} }
// Fetch and display own external IP for webUI // =========================================================================
// External IP
// =========================================================================
// Try multiple providers until one returns a valid IP
function displayExternalIP() { function displayExternalIP() {
const target = document.getElementById('external-ip'); const target = document.getElementById('external-ip');
if (!target) return; if (!target) return;
@@ -219,7 +230,10 @@ function displayExternalIP() {
tryProvider(0); tryProvider(0);
} }
// Function to initialize tooltips // =========================================================================
// UI Initialization
// =========================================================================
function initializeTooltips() { function initializeTooltips() {
const tooltips = document.querySelectorAll('[data-tooltip]'); const tooltips = document.querySelectorAll('[data-tooltip]');
tooltips.forEach(el => { tooltips.forEach(el => {
@@ -245,7 +259,7 @@ function initializeTooltips() {
}); });
} }
// Function to initialize the IP search // Restrict the IP search input to digits and dots only
function initializeSearch() { function initializeSearch() {
const ipSearch = document.getElementById("ipSearch"); const ipSearch = document.getElementById("ipSearch");
if (ipSearch) { if (ipSearch) {
@@ -258,11 +272,14 @@ function initializeSearch() {
} }
} }
// Update restart banner visibility // =========================================================================
// Navigation
// =========================================================================
function updateRestartBanner() { function updateRestartBanner() {
var banner = document.getElementById('restartBanner'); var banner = document.getElementById('restartBanner');
if (!banner) return; if (!banner) return;
// Don't show restart banner for local connectors - they only reload, not restart // Don't show restart banner for local connectors; they only reload, not restart
if (currentServer && currentServer.restartNeeded && currentServer.type !== 'local') { if (currentServer && currentServer.restartNeeded && currentServer.type !== 'local') {
banner.style.display = 'block'; banner.style.display = 'block';
} else { } else {
@@ -270,7 +287,6 @@ function updateRestartBanner() {
} }
} }
// Load dynamically the other pages when navigating in nav
function showSection(sectionId) { function showSection(sectionId) {
// hide all sections // hide all sections
document.getElementById('dashboardSection').classList.add('hidden'); document.getElementById('dashboardSection').classList.add('hidden');
@@ -292,14 +308,11 @@ function showSection(sectionId) {
loadSettings(); loadSettings();
} }
} }
// Close navbar on mobile when clicking a menu item // Close navbar on mobile when clicking a menu item
document.getElementById('mobileMenu').classList.add('hidden'); document.getElementById('mobileMenu').classList.add('hidden');
} }
// Toggle mobile menu
function toggleMobileMenu() { function toggleMobileMenu() {
const menu = document.getElementById('mobileMenu'); const menu = document.getElementById('mobileMenu');
menu.classList.toggle('hidden'); menu.classList.toggle('hidden');
} }

View File

@@ -1,30 +1,28 @@
// WebSocket Manager for Fail2ban UI // WebSocket manager for real-time updates in Fail2ban UI.
// Handles real-time communication with the backend
"use strict"; "use strict";
// =========================================================================
// WebSocket Manager
// =========================================================================
class WebSocketManager { class WebSocketManager {
constructor() { constructor() {
this.ws = null; this.ws = null;
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
this.maxReconnectAttempts = Infinity; this.maxReconnectAttempts = Infinity;
this.reconnectDelay = 1000; // Start with 1 second this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000; // Max 30 seconds this.maxReconnectDelay = 30000;
this.isConnecting = false; this.isConnecting = false;
this.isConnected = false; this.isConnected = false;
this.lastBanEventId = null; this.lastBanEventId = null;
this.statusCallbacks = []; this.statusCallbacks = [];
this.banEventCallbacks = []; this.banEventCallbacks = [];
this.consoleLogCallbacks = []; this.consoleLogCallbacks = [];
// Connection metrics for tooltip
this.connectedAt = null; this.connectedAt = null;
this.lastHeartbeatAt = null; this.lastHeartbeatAt = null;
this.messageCount = 0; this.messageCount = 0;
this.totalReconnects = 0; this.totalReconnects = 0;
this.initialConnection = true; this.initialConnection = true;
// Get WebSocket URL
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host; const host = window.location.host;
this.wsUrl = `${protocol}//${host}/api/ws`; this.wsUrl = `${protocol}//${host}/api/ws`;
@@ -86,8 +84,6 @@ class WebSocketManager {
this.isConnected = false; this.isConnected = false;
this.updateStatus('disconnected', 'Disconnected'); this.updateStatus('disconnected', 'Disconnected');
console.log('WebSocket disconnected'); console.log('WebSocket disconnected');
// Attempt to reconnect
if (this.reconnectAttempts < this.maxReconnectAttempts) { if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.scheduleReconnect(); this.scheduleReconnect();
} }
@@ -117,7 +113,7 @@ class WebSocketManager {
this.handleBanEvent(message.data); this.handleBanEvent(message.data);
break; break;
case 'unban_event': case 'unban_event':
this.handleBanEvent(message.data); // Use same handler for unban events this.handleBanEvent(message.data);
break; break;
case 'heartbeat': case 'heartbeat':
this.handleHeartbeat(message); this.handleHeartbeat(message);
@@ -131,7 +127,6 @@ class WebSocketManager {
} }
handleConsoleLog(message) { handleConsoleLog(message) {
// Notify all registered console log callbacks
if (this.consoleLogCallbacks) { if (this.consoleLogCallbacks) {
this.consoleLogCallbacks.forEach(callback => { this.consoleLogCallbacks.forEach(callback => {
try { try {
@@ -145,22 +140,16 @@ class WebSocketManager {
handleBanEvent(eventData) { handleBanEvent(eventData) {
// Check if we've already processed this event (prevent duplicates) // Check if we've already processed this event (prevent duplicates)
// Only check if event has an ID and we have a lastBanEventId
if (eventData.id && this.lastBanEventId !== null && eventData.id <= this.lastBanEventId) { if (eventData.id && this.lastBanEventId !== null && eventData.id <= this.lastBanEventId) {
console.log('Skipping duplicate ban event:', eventData.id); console.log('Skipping duplicate ban event:', eventData.id);
return; return;
} }
// Update lastBanEventId if event has an ID
if (eventData.id) { if (eventData.id) {
if (this.lastBanEventId === null || eventData.id > this.lastBanEventId) { if (this.lastBanEventId === null || eventData.id > this.lastBanEventId) {
this.lastBanEventId = eventData.id; this.lastBanEventId = eventData.id;
} }
} }
console.log('Processing ban event:', eventData); console.log('Processing ban event:', eventData);
// Notify all registered callbacks
this.banEventCallbacks.forEach(callback => { this.banEventCallbacks.forEach(callback => {
try { try {
callback(eventData); callback(eventData);
@@ -171,7 +160,6 @@ class WebSocketManager {
} }
handleHeartbeat(message) { handleHeartbeat(message) {
// Update status to show backend is healthy
this.lastHeartbeatAt = new Date(); this.lastHeartbeatAt = new Date();
if (this.isConnected) { if (this.isConnected) {
this.updateStatus('connected', 'Connected'); this.updateStatus('connected', 'Connected');
@@ -276,12 +264,13 @@ class WebSocketManager {
} }
} }
// Create global instance - initialize immediately // =========================================================================
// Create Global Instance of WebSocketManager
// =========================================================================
var wsManager = null; var wsManager = null;
// Initialize WebSocket manager
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
// Initialize immediately if DOM is already loaded, otherwise wait
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
if (!wsManager) { if (!wsManager) {
@@ -292,4 +281,3 @@ if (typeof window !== 'undefined') {
wsManager = new WebSocketManager(); wsManager = new WebSocketManager();
} }
} }