mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
Add some status-tooltip implementation for the websocket connection
This commit is contained in:
@@ -430,4 +430,46 @@ button.bg-red-500:hover, button.bg-red-600:hover {
|
||||
.text-green-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(22 101 52 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
/* WebSocket Tooltip Styling */
|
||||
#wsTooltip {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#wsTooltip .text-green-400 {
|
||||
color: rgb(74 222 128);
|
||||
}
|
||||
|
||||
#wsTooltip .text-blue-400 {
|
||||
color: rgb(96 165 250);
|
||||
}
|
||||
|
||||
#wsTooltip .text-yellow-400 {
|
||||
color: rgb(250 204 21);
|
||||
}
|
||||
|
||||
#wsTooltip .text-orange-400 {
|
||||
color: rgb(251 146 60);
|
||||
}
|
||||
|
||||
#wsTooltip .text-gray-400 {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
#wsTooltip .text-gray-500 {
|
||||
color: rgb(107 114 128);
|
||||
}
|
||||
|
||||
#wsTooltip .border-gray-700 {
|
||||
border-color: rgb(55 65 81);
|
||||
}
|
||||
|
||||
#wsTooltip .border-b {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
#wsTooltip .border-t {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
@@ -86,10 +86,138 @@ function initStatusIndicator() {
|
||||
}
|
||||
}
|
||||
|
||||
// Create and manage WebSocket tooltip
|
||||
function createWebSocketTooltip() {
|
||||
// Create tooltip element
|
||||
const tooltip = document.createElement('div');
|
||||
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.style.display = 'none';
|
||||
tooltip.style.minWidth = '200px';
|
||||
document.body.appendChild(tooltip);
|
||||
|
||||
const statusEl = document.getElementById('backendStatus');
|
||||
if (!statusEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tooltipUpdateInterval = null;
|
||||
|
||||
function updateTooltipContent() {
|
||||
if (!wsManager || !wsManager.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = wsManager.getConnectionInfo();
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
tooltip.innerHTML = `
|
||||
<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="flex justify-between">
|
||||
<span class="text-gray-400">Duration:</span>
|
||||
<span class="text-green-400 font-medium">${info.duration}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-400">Last Heartbeat:</span>
|
||||
<span class="text-blue-400 font-medium">${info.lastHeartbeat}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-400">Messages:</span>
|
||||
<span class="text-yellow-400 font-medium">${info.messages}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-400">Reconnects:</span>
|
||||
<span class="text-orange-400 font-medium">${info.reconnects}</span>
|
||||
</div>
|
||||
<div class="mt-2 pt-2 border-t border-gray-700">
|
||||
<div class="text-gray-400 text-xs">${info.protocol}</div>
|
||||
<div class="text-gray-500 text-xs mt-1 break-all">${info.url}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function showTooltip(e) {
|
||||
if (!wsManager || !wsManager.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateTooltipContent();
|
||||
const rect = statusEl.getBoundingClientRect();
|
||||
const tooltipRect = tooltip.getBoundingClientRect();
|
||||
|
||||
// Position tooltip below the status element, centered
|
||||
let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
|
||||
let top = rect.bottom + 8;
|
||||
|
||||
// Adjust if tooltip would go off screen
|
||||
if (left < 8) left = 8;
|
||||
if (left + tooltipRect.width > window.innerWidth - 8) {
|
||||
left = window.innerWidth - tooltipRect.width - 8;
|
||||
}
|
||||
if (top + tooltipRect.height > window.innerHeight - 8) {
|
||||
top = rect.top - tooltipRect.height - 8;
|
||||
}
|
||||
|
||||
tooltip.style.left = left + 'px';
|
||||
tooltip.style.top = top + 'px';
|
||||
tooltip.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
tooltip.style.opacity = '1';
|
||||
}, 10);
|
||||
|
||||
// Update tooltip content every second while visible
|
||||
if (tooltipUpdateInterval) {
|
||||
clearInterval(tooltipUpdateInterval);
|
||||
}
|
||||
tooltipUpdateInterval = setInterval(updateTooltipContent, 1000);
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
tooltip.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
tooltip.style.display = 'none';
|
||||
}, 200);
|
||||
|
||||
if (tooltipUpdateInterval) {
|
||||
clearInterval(tooltipUpdateInterval);
|
||||
tooltipUpdateInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
statusEl.addEventListener('mouseenter', showTooltip);
|
||||
statusEl.addEventListener('mouseleave', hideTooltip);
|
||||
|
||||
// Also hide tooltip when status changes to disconnected
|
||||
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||
wsManager.onStatusChange(function(state, text) {
|
||||
if (state !== 'connected') {
|
||||
hideTooltip();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Wait for WebSocket manager to be available
|
||||
var checkInterval = setInterval(function() {
|
||||
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||
wsManager.onStatusChange(function(state, text) {
|
||||
if (state !== 'connected') {
|
||||
hideTooltip();
|
||||
}
|
||||
});
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize all header components
|
||||
function initHeader() {
|
||||
initClock();
|
||||
initStatusIndicator();
|
||||
createWebSocketTooltip();
|
||||
}
|
||||
|
||||
// Cleanup on page unload
|
||||
|
||||
@@ -16,6 +16,13 @@ class WebSocketManager {
|
||||
this.statusCallbacks = [];
|
||||
this.banEventCallbacks = [];
|
||||
|
||||
// Connection metrics for tooltip
|
||||
this.connectedAt = null;
|
||||
this.lastHeartbeatAt = null;
|
||||
this.messageCount = 0;
|
||||
this.totalReconnects = 0;
|
||||
this.initialConnection = true;
|
||||
|
||||
// Get WebSocket URL
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = window.location.host;
|
||||
@@ -38,6 +45,11 @@ class WebSocketManager {
|
||||
this.ws.onopen = () => {
|
||||
this.isConnecting = false;
|
||||
this.isConnected = true;
|
||||
this.connectedAt = new Date();
|
||||
if (!this.initialConnection) {
|
||||
this.totalReconnects++;
|
||||
}
|
||||
this.initialConnection = false;
|
||||
this.reconnectAttempts = 0;
|
||||
this.reconnectDelay = 1000;
|
||||
this.updateStatus('connected', 'Connected');
|
||||
@@ -47,6 +59,7 @@ class WebSocketManager {
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
this.messageCount++;
|
||||
this.handleMessage(message);
|
||||
} catch (err) {
|
||||
console.error('Error parsing WebSocket message:', err);
|
||||
@@ -133,6 +146,7 @@ class WebSocketManager {
|
||||
|
||||
handleHeartbeat(message) {
|
||||
// Update status to show backend is healthy
|
||||
this.lastHeartbeatAt = new Date();
|
||||
if (this.isConnected) {
|
||||
this.updateStatus('connected', 'Connected');
|
||||
}
|
||||
@@ -187,6 +201,46 @@ class WebSocketManager {
|
||||
isHealthy() {
|
||||
return this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
formatDuration(seconds) {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
if (seconds < 3600) {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}m ${secs}s`;
|
||||
}
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 60);
|
||||
return `${hours}h ${mins}m`;
|
||||
}
|
||||
|
||||
getConnectionInfo() {
|
||||
if (!this.isConnected || !this.connectedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const duration = Math.floor((now - this.connectedAt) / 1000);
|
||||
const durationStr = this.formatDuration(duration);
|
||||
|
||||
const lastHeartbeat = this.lastHeartbeatAt
|
||||
? Math.floor((now - this.lastHeartbeatAt) / 1000)
|
||||
: null;
|
||||
const heartbeatStr = lastHeartbeat !== null
|
||||
? (lastHeartbeat < 60 ? `${lastHeartbeat}s ago` : `${Math.floor(lastHeartbeat / 60)}m ago`)
|
||||
: 'Never';
|
||||
|
||||
const protocol = this.wsUrl.startsWith('wss:') ? 'WSS (Secure)' : 'WS';
|
||||
|
||||
return {
|
||||
duration: durationStr,
|
||||
lastHeartbeat: heartbeatStr,
|
||||
url: this.wsUrl,
|
||||
messages: this.messageCount,
|
||||
reconnects: this.totalReconnects,
|
||||
protocol: protocol
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance - initialize immediately
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="text-xl font-bold">Fail2ban UI</span>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user