mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 05:53:15 +02:00
Implemented a real-time console log streaming via WebSocket for debugging purposes. Users can enable console output in settings to view application logs directly in the web interface.
This commit is contained in:
@@ -462,6 +462,11 @@ button.bg-red-500:hover, button.bg-red-600:hover {
|
||||
color: rgb(107 114 128);
|
||||
}
|
||||
|
||||
/* Console output text colors */
|
||||
.text-gray-100 {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
#wsTooltip .border-gray-700 {
|
||||
border-color: rgb(55 65 81);
|
||||
}
|
||||
|
||||
213
pkg/web/static/js/console.js
Normal file
213
pkg/web/static/js/console.js
Normal file
@@ -0,0 +1,213 @@
|
||||
// Console Output Handler for Fail2ban UI
|
||||
"use strict";
|
||||
|
||||
let consoleOutputContainer = null;
|
||||
let consoleOutputElement = null;
|
||||
let maxConsoleLines = 1000; // Maximum number of lines to keep in console
|
||||
let wasConsoleEnabledOnLoad = false; // Track if console was enabled when page loaded
|
||||
|
||||
function initConsoleOutput() {
|
||||
consoleOutputContainer = document.getElementById('consoleOutputContainer');
|
||||
consoleOutputElement = document.getElementById('consoleOutputWindow');
|
||||
|
||||
if (!consoleOutputContainer || !consoleOutputElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register WebSocket callback for console logs
|
||||
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||
wsManager.onConsoleLog(function(message, timestamp) {
|
||||
appendConsoleLog(message, timestamp);
|
||||
});
|
||||
} else {
|
||||
// Wait for WebSocket manager to be available
|
||||
const wsCheckInterval = setInterval(function() {
|
||||
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||
wsManager.onConsoleLog(function(message, timestamp) {
|
||||
appendConsoleLog(message, timestamp);
|
||||
});
|
||||
clearInterval(wsCheckInterval);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Stop checking after 5 seconds
|
||||
setTimeout(function() {
|
||||
clearInterval(wsCheckInterval);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleConsoleOutput(userClicked) {
|
||||
const checkbox = document.getElementById('consoleOutput');
|
||||
const container = document.getElementById('consoleOutputContainer');
|
||||
|
||||
if (!checkbox || !container) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkbox.checked) {
|
||||
container.classList.remove('hidden');
|
||||
// Initialize console if not already done
|
||||
if (!consoleOutputElement) {
|
||||
initConsoleOutput();
|
||||
} else {
|
||||
// Re-register WebSocket callback in case it wasn't registered before
|
||||
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||
// Remove any existing callbacks and re-register
|
||||
if (!wsManager.consoleLogCallbacks) {
|
||||
wsManager.consoleLogCallbacks = [];
|
||||
}
|
||||
// Check if callback already exists
|
||||
let callbackExists = false;
|
||||
for (let i = 0; i < wsManager.consoleLogCallbacks.length; i++) {
|
||||
if (wsManager.consoleLogCallbacks[i].toString().includes('appendConsoleLog')) {
|
||||
callbackExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!callbackExists) {
|
||||
wsManager.onConsoleLog(function(message, timestamp) {
|
||||
appendConsoleLog(message, timestamp);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show save hint only if user just clicked to enable (not on page load)
|
||||
const consoleEl = document.getElementById('consoleOutputWindow');
|
||||
if (consoleEl && userClicked && !wasConsoleEnabledOnLoad) {
|
||||
// Clear initial placeholder message
|
||||
const placeholder = consoleEl.querySelector('.text-gray-500');
|
||||
if (placeholder && placeholder.textContent === 'Console output will appear here...') {
|
||||
placeholder.remove();
|
||||
}
|
||||
|
||||
// Show save hint message
|
||||
const hintDiv = document.createElement('div');
|
||||
hintDiv.className = 'text-yellow-400 italic text-center py-4';
|
||||
hintDiv.id = 'consoleSaveHint';
|
||||
const hintText = typeof t !== 'undefined' ? t('settings.console.save_hint', 'Please save your settings first before logs will be displayed here.') : 'Please save your settings first before logs will be displayed here.';
|
||||
hintDiv.textContent = hintText;
|
||||
consoleEl.appendChild(hintDiv);
|
||||
} else if (consoleEl) {
|
||||
// Just clear initial placeholder if it exists
|
||||
const placeholder = consoleEl.querySelector('.text-gray-500');
|
||||
if (placeholder && placeholder.textContent === 'Console output will appear here...') {
|
||||
placeholder.remove();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
container.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function appendConsoleLog(message, timestamp) {
|
||||
if (!consoleOutputElement) {
|
||||
consoleOutputElement = document.getElementById('consoleOutputWindow');
|
||||
}
|
||||
|
||||
if (!consoleOutputElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove initial placeholder message
|
||||
const placeholder = consoleOutputElement.querySelector('.text-gray-500');
|
||||
if (placeholder && placeholder.textContent === 'Console output will appear here...') {
|
||||
placeholder.remove();
|
||||
}
|
||||
|
||||
// Remove save hint when first log arrives
|
||||
const saveHint = document.getElementById('consoleSaveHint');
|
||||
if (saveHint) {
|
||||
saveHint.remove();
|
||||
}
|
||||
|
||||
// Create log line
|
||||
const logLine = document.createElement('div');
|
||||
logLine.className = 'text-green-400 leading-relaxed';
|
||||
|
||||
// Format timestamp if provided
|
||||
let timeStr = '';
|
||||
if (timestamp) {
|
||||
try {
|
||||
const date = new Date(timestamp);
|
||||
timeStr = '<span class="text-gray-500">[' + date.toLocaleTimeString() + ']</span> ';
|
||||
} catch (e) {
|
||||
// Ignore timestamp parsing errors
|
||||
}
|
||||
}
|
||||
|
||||
// Escape HTML to prevent XSS (but preserve timestamp HTML)
|
||||
let escapedMessage = message;
|
||||
if (typeof escapeHtml === 'function') {
|
||||
escapedMessage = escapeHtml(escapedMessage);
|
||||
} else {
|
||||
// Fallback HTML escaping
|
||||
escapedMessage = escapedMessage
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Color code different log levels
|
||||
let logClass = 'text-green-400';
|
||||
const msgLower = message.toLowerCase();
|
||||
if (msgLower.includes('error') || msgLower.includes('fatal')) {
|
||||
logClass = 'text-red-400';
|
||||
} else if (msgLower.includes('warning') || msgLower.includes('warn')) {
|
||||
logClass = 'text-yellow-400';
|
||||
} else if (msgLower.includes('info') || msgLower.includes('debug')) {
|
||||
logClass = 'text-blue-400';
|
||||
}
|
||||
|
||||
logLine.className = logClass + ' leading-relaxed';
|
||||
logLine.innerHTML = timeStr + escapedMessage;
|
||||
|
||||
// Add to console
|
||||
consoleOutputElement.appendChild(logLine);
|
||||
|
||||
// Limit number of lines
|
||||
const lines = consoleOutputElement.children;
|
||||
if (lines.length > maxConsoleLines) {
|
||||
consoleOutputElement.removeChild(lines[0]);
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom
|
||||
consoleOutputElement.scrollTop = consoleOutputElement.scrollHeight;
|
||||
}
|
||||
|
||||
function clearConsole() {
|
||||
if (!consoleOutputElement) {
|
||||
consoleOutputElement = document.getElementById('consoleOutputWindow');
|
||||
}
|
||||
|
||||
if (consoleOutputElement) {
|
||||
consoleOutputElement.textContent = '';
|
||||
// Note: wasConsoleEnabledOnLoad remains true, so hint won't show again after clear
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
if (typeof window !== 'undefined') {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Check if console output is already enabled on page load
|
||||
const checkbox = document.getElementById('consoleOutput');
|
||||
if (checkbox && checkbox.checked) {
|
||||
wasConsoleEnabledOnLoad = true; // Mark that it was enabled on load
|
||||
toggleConsoleOutput(false); // false = not a user click
|
||||
}
|
||||
initConsoleOutput();
|
||||
});
|
||||
} else {
|
||||
// Check if console output is already enabled on page load
|
||||
const checkbox = document.getElementById('consoleOutput');
|
||||
if (checkbox && checkbox.checked) {
|
||||
wasConsoleEnabledOnLoad = true; // Mark that it was enabled on load
|
||||
toggleConsoleOutput(false); // false = not a user click
|
||||
}
|
||||
initConsoleOutput();
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,15 @@ function loadSettings() {
|
||||
}
|
||||
|
||||
document.getElementById('debugMode').checked = data.debug || false;
|
||||
const consoleOutputEl = document.getElementById('consoleOutput');
|
||||
if (consoleOutputEl) {
|
||||
consoleOutputEl.checked = data.consoleOutput || false;
|
||||
// Mark that console was enabled on load (settings were already saved)
|
||||
if (typeof wasConsoleEnabledOnLoad !== 'undefined') {
|
||||
wasConsoleEnabledOnLoad = consoleOutputEl.checked;
|
||||
}
|
||||
toggleConsoleOutput(false); // false = not a user click, loading from saved settings
|
||||
}
|
||||
|
||||
// Set callback URL and add auto-update listener for port changes
|
||||
const callbackURLInput = document.getElementById('callbackURL');
|
||||
@@ -201,6 +210,7 @@ function saveSettings(event) {
|
||||
language: document.getElementById('languageSelect').value,
|
||||
port: currentPort,
|
||||
debug: document.getElementById('debugMode').checked,
|
||||
consoleOutput: document.getElementById('consoleOutput') ? document.getElementById('consoleOutput').checked : false,
|
||||
destemail: document.getElementById('destEmail').value.trim(),
|
||||
callbackUrl: callbackUrl,
|
||||
callbackSecret: document.getElementById('callbackSecret').value.trim(),
|
||||
|
||||
@@ -15,6 +15,7 @@ class WebSocketManager {
|
||||
this.lastBanEventId = null;
|
||||
this.statusCallbacks = [];
|
||||
this.banEventCallbacks = [];
|
||||
this.consoleLogCallbacks = [];
|
||||
|
||||
// Connection metrics for tooltip
|
||||
this.connectedAt = null;
|
||||
@@ -58,11 +59,20 @@ class WebSocketManager {
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
this.messageCount++;
|
||||
this.handleMessage(message);
|
||||
// WebSocket may send multiple JSON messages separated by newlines
|
||||
// Split by newlines and parse each message separately
|
||||
const messages = event.data.split('\n').filter(line => line.trim().length > 0);
|
||||
for (const messageText of messages) {
|
||||
try {
|
||||
const message = JSON.parse(messageText);
|
||||
this.messageCount++;
|
||||
this.handleMessage(message);
|
||||
} catch (parseErr) {
|
||||
console.error('Error parsing individual WebSocket message:', parseErr, 'Raw:', messageText);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing WebSocket message:', err);
|
||||
console.error('Error processing WebSocket message:', err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,11 +122,27 @@ class WebSocketManager {
|
||||
case 'heartbeat':
|
||||
this.handleHeartbeat(message);
|
||||
break;
|
||||
case 'console_log':
|
||||
this.handleConsoleLog(message);
|
||||
break;
|
||||
default:
|
||||
console.log('Unknown message type:', message.type);
|
||||
}
|
||||
}
|
||||
|
||||
handleConsoleLog(message) {
|
||||
// Notify all registered console log callbacks
|
||||
if (this.consoleLogCallbacks) {
|
||||
this.consoleLogCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(message.message, message.time);
|
||||
} catch (err) {
|
||||
console.error('Error in console log callback:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleBanEvent(eventData) {
|
||||
// Check if we've already processed this event (prevent duplicates)
|
||||
// Only check if event has an ID and we have a lastBanEventId
|
||||
@@ -170,6 +196,13 @@ class WebSocketManager {
|
||||
this.banEventCallbacks.push(callback);
|
||||
}
|
||||
|
||||
onConsoleLog(callback) {
|
||||
if (!this.consoleLogCallbacks) {
|
||||
this.consoleLogCallbacks = [];
|
||||
}
|
||||
this.consoleLogCallbacks.push(callback);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
|
||||
Reference in New Issue
Block a user