// Database Status Monitor class DatabaseStatusMonitor { constructor() { this.updateInterval = 5000; // 5 seconds this.intervalId = null; this.elements = { connection: document.getElementById('dbConnection'), latency: document.getElementById('dbLatency'), write: document.getElementById('dbWrite'), timestamp: document.getElementById('dbTimestamp'), retryBtn: document.getElementById('dbRetryBtn'), statusBar: document.getElementById('dbStatusBar'), detailsPanel: document.getElementById('dbStatusDetails'), errorBanner: document.getElementById('sqlErrorBanner'), errorMessage: document.getElementById('sqlErrorMessage'), errorDetails: document.getElementById('sqlErrorDetails'), version: { container: document.getElementById('gitVersion'), hash: document.getElementById('commitHash'), age: document.getElementById('commitAge') } }; this.logs = []; this.maxLogs = 50; // Make status bar clickable to show details if (this.elements.statusBar) { this.elements.statusBar.style.cursor = 'pointer'; this.elements.statusBar.addEventListener('click', (e) => { // Don't toggle if clicking the retry button if (!e.target.closest('.db-retry-btn')) { this.toggleDetails(); } }); } } addLog(message, type = 'info') { const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); const logEntry = { timestamp, message, type // 'info', 'success', 'error', 'warning' }; this.logs.unshift(logEntry); if (this.logs.length > this.maxLogs) { this.logs.pop(); } // Console logging with emoji const emoji = { 'info': 'ℹ️', 'success': '✅', 'error': '❌', 'warning': '⚠️' }; console.log(`${emoji[type]} [DB-STATUS ${timestamp}] ${message}`); // Update logs display this.updateLogsDisplay(); } updateLogsDisplay() { const logsContainer = document.getElementById('detailLogs'); if (!logsContainer) return; logsContainer.innerHTML = '
Recent Logs:
'; this.logs.slice(0, 20).forEach(log => { const logDiv = document.createElement('div'); logDiv.className = `db-log-entry db-log-${log.type}`; logDiv.textContent = `[${log.timestamp}] ${log.message}`; logsContainer.appendChild(logDiv); }); } toggleDetails() { if (this.elements.detailsPanel) { const isHidden = this.elements.detailsPanel.style.display === 'none'; this.elements.detailsPanel.style.display = isHidden ? 'block' : 'none'; if (isHidden) { this.addLog('Details panel opened', 'info'); } } } async checkStatus() { this.addLog('Checking database status...', 'info'); try { const response = await fetch('/api/db-status'); const data = await response.json(); this.addLog(`Response received: ${data.connected ? 'Connected' : 'Disconnected'}`, data.connected ? 'success' : 'error'); if (data.error) { this.addLog(`Error: ${data.error}`, 'error'); } if (data.poolStats) { this.addLog(`Pool: ${data.poolStats.freeConnections}/${data.poolStats.totalConnections} free, ${data.poolStats.queuedRequests} queued`, 'info'); } this.updateUI(data); this.updateDetails(data); this.updateErrorBanner(data); } catch (error) { this.addLog(`Failed to fetch status: ${error.message}`, 'error'); console.error('Failed to fetch database status:', error); const errorData = { connected: false, latency: 0, writeCapable: false, error: 'Failed to connect to server' }; this.updateUI(errorData); this.updateErrorBanner(errorData); } } updateErrorBanner(status) { if (!this.elements.errorBanner) return; if (!status.connected || status.error) { // Show error banner this.elements.errorBanner.style.display = 'block'; // Update error message if (this.elements.errorMessage) { this.elements.errorMessage.textContent = status.error || 'Connection failed'; } // Update error details if (this.elements.errorDetails) { const details = []; if (status.host) { details.push(`🖥️ Host: ${status.host}`); } if (status.database) { details.push(`🗄️ Database: ${status.database}`); } if (status.user) { details.push(`👤 User: ${status.user}`); } if (status.latency !== undefined) { details.push(`⏱️ Latency: ${status.latency}ms`); } if (status.poolStats) { details.push(`🔗 Pool: ${status.poolStats.freeConnections}/${status.poolStats.totalConnections} free`); } details.push(`🕐 Last Check: ${new Date().toLocaleTimeString()}`); this.elements.errorDetails.innerHTML = details.map(d => `
${d}
` ).join(''); } } else { // Hide error banner when connected this.elements.errorBanner.style.display = 'none'; } } updateDetails(status) { // Update detail panel const detailStatus = document.getElementById('detailStatus'); const detailHost = document.getElementById('detailHost'); const detailDatabase = document.getElementById('detailDatabase'); const detailError = document.getElementById('detailError'); if (detailStatus) { detailStatus.textContent = status.connected ? '✅ Connected' : '❌ Disconnected'; detailStatus.style.color = status.connected ? '#10b981' : '#ef4444'; } if (detailHost) { detailHost.textContent = status.host || 'unknown'; } if (detailDatabase) { detailDatabase.textContent = status.database || 'unknown'; } if (detailError) { if (status.error) { detailError.textContent = status.error; detailError.style.color = '#ef4444'; } else { detailError.textContent = 'None'; detailError.style.color = '#10b981'; } } } updateUI(status) { // Update connection status const connectionDot = this.elements.connection.querySelector('.status-dot'); const connectionText = this.elements.connection.querySelector('.status-text'); if (status.connected) { connectionDot.className = 'status-dot status-connected'; connectionText.textContent = 'Connected'; this.elements.connection.classList.remove('status-error'); this.elements.connection.classList.add('status-success'); } else { connectionDot.className = 'status-dot status-disconnected'; connectionText.textContent = 'Disconnected'; this.elements.connection.classList.remove('status-success'); this.elements.connection.classList.add('status-error'); } // Update latency if (status.connected && status.latency > 0) { this.elements.latency.textContent = `${status.latency} ms`; // Color code based on latency if (status.latency < 50) { this.elements.latency.className = 'db-status-value latency-good'; } else if (status.latency < 150) { this.elements.latency.className = 'db-status-value latency-ok'; } else { this.elements.latency.className = 'db-status-value latency-slow'; } } else { this.elements.latency.textContent = '-- ms'; this.elements.latency.className = 'db-status-value'; } // Update write capability const writeDot = this.elements.write.querySelector('.status-dot'); const writeText = this.elements.write.querySelector('.status-text'); if (status.writeCapable) { writeDot.className = 'status-dot status-connected'; writeText.textContent = 'Enabled'; this.elements.write.classList.remove('status-error', 'status-warning'); this.elements.write.classList.add('status-success'); } else if (status.connected) { writeDot.className = 'status-dot status-warning'; writeText.textContent = 'Read-Only'; this.elements.write.classList.remove('status-error', 'status-success'); this.elements.write.classList.add('status-warning'); } else { writeDot.className = 'status-dot status-disconnected'; writeText.textContent = 'Disabled'; this.elements.write.classList.remove('status-success', 'status-warning'); this.elements.write.classList.add('status-error'); } // Update timestamp const now = new Date(); const timeString = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); this.elements.timestamp.textContent = timeString; } async retryConnection() { this.addLog('🔄 Manual retry triggered', 'info'); // Visual feedback on button if (this.elements.retryBtn) { this.elements.retryBtn.disabled = true; this.elements.retryBtn.classList.add('retrying'); const originalText = this.elements.retryBtn.innerHTML; this.elements.retryBtn.innerHTML = ` Retrying... `; await this.checkStatus(); setTimeout(() => { this.elements.retryBtn.disabled = false; this.elements.retryBtn.classList.remove('retrying'); this.elements.retryBtn.innerHTML = originalText; }, 1000); } else { await this.checkStatus(); } } async fetchVersion() { try { const response = await fetch('/api/version'); const data = await response.json(); if (data.hash && this.elements.version.container) { this.elements.version.container.style.display = 'flex'; this.elements.version.hash.textContent = data.hash; // Calculate age const now = Math.floor(Date.now() / 1000); const diff = now - data.timestamp; let ageText = ''; if (diff < 60) ageText = 'just now'; else if (diff < 3600) ageText = `${Math.floor(diff / 60)}m ago`; else if (diff < 86400) ageText = `${Math.floor(diff / 3600)}h ago`; else ageText = `${Math.floor(diff / 86400)}d ago`; this.elements.version.age.textContent = ageText; } } catch (e) { console.warn('Failed to fetch version:', e); } } start() { // Initial check this.addLog('Database status monitor started', 'success'); this.checkStatus(); this.fetchVersion(); // Set up periodic checks this.intervalId = setInterval(() => { this.checkStatus(); }, this.updateInterval); console.log('✅ Database status monitor started'); } stop() { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; this.addLog('Database status monitor stopped', 'warning'); console.log('⏹️ Database status monitor stopped'); } } } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.dbStatusMonitor = new DatabaseStatusMonitor(); window.dbStatusMonitor.start(); }); // Clean up on page unload window.addEventListener('beforeunload', () => { if (window.dbStatusMonitor) { window.dbStatusMonitor.stop(); } });