Enhanced SQL connection monitoring and diagnostics - Added detailed database status monitoring with retry functionality - Improved error handling and logging for SQL connections - Updated UI with connection status indicators and error banners

This commit is contained in:
2025-12-20 15:50:34 +11:00
parent e7c8d890b9
commit 5238fc8d22
4 changed files with 611 additions and 8 deletions

View File

@@ -7,24 +7,192 @@ class DatabaseStatusMonitor {
connection: document.getElementById('dbConnection'),
latency: document.getElementById('dbLatency'),
write: document.getElementById('dbWrite'),
timestamp: document.getElementById('dbTimestamp')
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')
};
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 = '<div class="db-logs-title">Recent Logs:</div>';
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);
this.updateUI({
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 =>
`<div class="sql-error-detail-item">${d}</div>`
).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';
}
}
}
@@ -94,8 +262,36 @@ class DatabaseStatusMonitor {
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 = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spinning">
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
</svg>
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();
}
}
start() {
// Initial check
this.addLog('Database status monitor started', 'success');
this.checkStatus();
// Set up periodic checks
@@ -110,6 +306,7 @@ class DatabaseStatusMonitor {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
this.addLog('Database status monitor stopped', 'warning');
console.log('⏹️ Database status monitor stopped');
}
}

View File

@@ -180,6 +180,53 @@
<span class="db-status-label">Last Check:</span>
<span class="db-status-value" id="dbTimestamp">--</span>
</div>
<div class="db-status-item">
<button class="db-retry-btn" id="dbRetryBtn" onclick="window.dbStatusMonitor && window.dbStatusMonitor.retryConnection()" title="Retry SQL Connection">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
</svg>
Retry
</button>
</div>
</div>
<!-- Database Status Details (for testing phase) -->
<div class="db-status-details" id="dbStatusDetails" style="display: none;">
<div class="db-details-header">
<span class="db-details-title">🔍 SQL Connection Details</span>
<button class="db-details-close" onclick="document.getElementById('dbStatusDetails').style.display='none'">×</button>
</div>
<div class="db-details-content" id="dbDetailsContent">
<div class="db-detail-item">
<span class="db-detail-label">Status:</span>
<span class="db-detail-value" id="detailStatus">--</span>
</div>
<div class="db-detail-item">
<span class="db-detail-label">Host:</span>
<span class="db-detail-value" id="detailHost">--</span>
</div>
<div class="db-detail-item">
<span class="db-detail-label">Database:</span>
<span class="db-detail-value" id="detailDatabase">--</span>
</div>
<div class="db-detail-item">
<span class="db-detail-label">Error:</span>
<span class="db-detail-value db-detail-error" id="detailError">None</span>
</div>
<div class="db-detail-logs" id="detailLogs"></div>
</div>
</div>
<!-- SQL Error Display (Testing Phase) -->
<div class="sql-error-banner" id="sqlErrorBanner" style="display: none;">
<div class="sql-error-header">
<span class="sql-error-icon">⚠️</span>
<span class="sql-error-title">SQL Connection Error</span>
</div>
<div class="sql-error-content">
<div class="sql-error-message" id="sqlErrorMessage">No errors</div>
<div class="sql-error-details" id="sqlErrorDetails"></div>
</div>
</div>
<footer class="footer">

View File

@@ -27,18 +27,28 @@ app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
// Database health check endpoint
// Database health check endpoint with detailed diagnostics
app.get('/api/db-status', async (req, res) => {
const startTime = Date.now();
const dbConfig = require('./db.config.js');
let status = {
connected: false,
latency: 0,
writeCapable: false,
timestamp: new Date().toISOString(),
error: null
error: null,
// Additional diagnostic info for testing phase
host: dbConfig.host || 'unknown',
database: dbConfig.database || 'unknown',
user: dbConfig.user || 'unknown',
connectionLimit: dbConfig.connectionLimit || 'unknown',
poolStats: null
};
try {
console.log(`[DB-STATUS] Testing connection to ${status.host}/${status.database}...`);
// Test connection with a simple query
const [result] = await db.pool.query('SELECT 1 as test');
const latency = Date.now() - startTime;
@@ -46,9 +56,23 @@ app.get('/api/db-status', async (req, res) => {
if (result && result[0].test === 1) {
status.connected = true;
status.latency = latency;
console.log(`[DB-STATUS] ✅ Connection successful (${latency}ms)`);
// Get pool statistics
try {
status.poolStats = {
totalConnections: db.pool.pool._allConnections.length,
freeConnections: db.pool.pool._freeConnections.length,
queuedRequests: db.pool.pool._connectionQueue.length
};
console.log(`[DB-STATUS] Pool stats:`, status.poolStats);
} catch (poolError) {
console.log(`[DB-STATUS] Could not retrieve pool stats:`, poolError.message);
}
// Test write capability
try {
console.log(`[DB-STATUS] Testing write capability...`);
const testTableName = '_health_check_test';
// Create test table if it doesn't exist
@@ -80,17 +104,34 @@ app.get('/api/db-status', async (req, res) => {
`);
status.writeCapable = true;
console.log(`[DB-STATUS] ✅ Write test successful`);
} catch (writeError) {
console.error('Write test failed:', writeError.message);
console.error(`[DB-STATUS] ❌ Write test failed:`, writeError.message);
status.writeCapable = false;
status.error = `Write test failed: ${writeError.message}`;
}
}
} catch (error) {
console.error('Database health check failed:', error.message);
console.error(`[DB-STATUS] ❌ Connection failed:`, error.message);
console.error(`[DB-STATUS] Error code:`, error.code);
console.error(`[DB-STATUS] Error errno:`, error.errno);
status.connected = false;
status.latency = Date.now() - startTime;
status.error = error.message;
// Provide more detailed error messages
let errorMessage = error.message;
if (error.code === 'ECONNREFUSED') {
errorMessage = `Connection refused to ${status.host}. Is MySQL running?`;
} else if (error.code === 'ER_ACCESS_DENIED_ERROR') {
errorMessage = `Access denied for user '${status.user}'. Check credentials.`;
} else if (error.code === 'ER_BAD_DB_ERROR') {
errorMessage = `Database '${status.database}' does not exist.`;
} else if (error.code === 'ENOTFOUND') {
errorMessage = `Host '${status.host}' not found. Check hostname.`;
}
status.error = errorMessage;
}
res.json(status);

View File

@@ -778,3 +778,321 @@ body {
margin-left: 0;
}
}
/* Database Retry Button */
.db-retry-btn {
padding: 0.5rem 1rem;
background: var(--accent-gradient);
border: none;
border-radius: 8px;
color: var(--text-primary);
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.3s ease;
font-family: "Inter", sans-serif;
display: flex;
align-items: center;
gap: 0.5rem;
box-shadow: 0 2px 8px rgba(147, 51, 234, 0.3);
}
.db-retry-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(147, 51, 234, 0.5);
}
.db-retry-btn:active:not(:disabled) {
transform: translateY(0);
}
.db-retry-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.db-retry-btn.retrying {
background: var(--bg-tertiary);
}
.db-retry-btn svg {
transition: transform 0.3s ease;
}
.db-retry-btn:hover:not(:disabled) svg {
transform: rotate(180deg);
}
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Database Status Details Panel */
.db-status-details {
background: var(--bg-secondary);
border: 1px solid var(--border-light);
border-radius: 12px;
margin: 1rem 0;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.db-details-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-light);
}
.db-details-title {
font-weight: 700;
font-size: 1rem;
color: var(--text-primary);
}
.db-details-close {
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 1.5rem;
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all 0.2s ease;
}
.db-details-close:hover {
background: var(--bg-primary);
color: var(--text-primary);
}
.db-details-content {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.db-detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: var(--bg-tertiary);
border-radius: 8px;
border: 1px solid var(--border-light);
}
.db-detail-label {
color: var(--text-secondary);
font-weight: 600;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.db-detail-value {
color: var(--text-primary);
font-weight: 600;
font-family: 'Courier New', monospace;
font-size: 0.95rem;
}
.db-detail-error {
max-width: 60%;
text-align: right;
word-wrap: break-word;
}
/* Database Logs */
.db-detail-logs {
margin-top: 1rem;
padding: 1rem;
background: var(--bg-primary);
border-radius: 8px;
border: 1px solid var(--border-light);
max-height: 300px;
overflow-y: auto;
}
.db-logs-title {
font-weight: 700;
color: var(--text-secondary);
margin-bottom: 0.75rem;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.db-log-entry {
padding: 0.5rem 0.75rem;
margin-bottom: 0.5rem;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
line-height: 1.4;
border-left: 3px solid transparent;
}
.db-log-info {
background: rgba(56, 189, 248, 0.05);
border-left-color: rgba(56, 189, 248, 0.5);
color: #38bdf8;
}
.db-log-success {
background: rgba(16, 185, 129, 0.05);
border-left-color: rgba(16, 185, 129, 0.5);
color: #10b981;
}
.db-log-error {
background: rgba(239, 68, 68, 0.05);
border-left-color: rgba(239, 68, 68, 0.5);
color: #ef4444;
}
.db-log-warning {
background: rgba(245, 158, 11, 0.05);
border-left-color: rgba(245, 158, 11, 0.5);
color: #f59e0b;
}
/* Scrollbar styling for logs */
.db-detail-logs::-webkit-scrollbar {
width: 8px;
}
.db-detail-logs::-webkit-scrollbar-track {
background: var(--bg-tertiary);
border-radius: 4px;
}
.db-detail-logs::-webkit-scrollbar-thumb {
background: var(--border-medium);
border-radius: 4px;
}
.db-detail-logs::-webkit-scrollbar-thumb:hover {
background: var(--accent-primary);
}
/* SQL Error Banner (Testing Phase) */
.sql-error-banner {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(245, 158, 11, 0.1));
border: 2px solid #ef4444;
border-radius: 12px;
margin: 1rem 0;
overflow: hidden;
box-shadow: 0 4px 20px rgba(239, 68, 68, 0.3);
animation: errorPulse 2s ease-in-out infinite;
}
@keyframes errorPulse {
0%, 100% {
box-shadow: 0 4px 20px rgba(239, 68, 68, 0.3);
}
50% {
box-shadow: 0 4px 30px rgba(239, 68, 68, 0.5);
}
}
.sql-error-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.5rem;
background: rgba(239, 68, 68, 0.15);
border-bottom: 1px solid rgba(239, 68, 68, 0.3);
}
.sql-error-icon {
font-size: 1.5rem;
animation: shake 0.5s ease-in-out infinite;
}
@keyframes shake {
0%, 100% {
transform: translateX(0);
}
25% {
transform: translateX(-3px);
}
75% {
transform: translateX(3px);
}
}
.sql-error-title {
font-weight: 700;
font-size: 1.1rem;
color: #ef4444;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.sql-error-content {
padding: 1.5rem;
}
.sql-error-message {
font-size: 1rem;
font-weight: 600;
color: #ef4444;
margin-bottom: 1rem;
padding: 1rem;
background: rgba(239, 68, 68, 0.1);
border-radius: 8px;
border-left: 4px solid #ef4444;
font-family: 'Courier New', monospace;
}
.sql-error-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem;
}
.sql-error-detail-item {
padding: 0.75rem;
background: var(--bg-tertiary);
border-radius: 6px;
border: 1px solid var(--border-light);
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 500;
}
@media (max-width: 768px) {
.sql-error-details {
grid-template-columns: 1fr;
}
}