mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 05:53:15 +02:00
Implement sections to core.js and websocket.js
This commit is contained in:
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user