Add database status bar with connection, latency, and write capability monitoring

This commit is contained in:
2025-12-15 21:20:29 +11:00
parent bcf3b0a032
commit e7c8d890b9
4 changed files with 392 additions and 1 deletions

129
db-status.js Normal file
View File

@@ -0,0 +1,129 @@
// 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')
};
}
async checkStatus() {
try {
const response = await fetch('/api/db-status');
const data = await response.json();
this.updateUI(data);
} catch (error) {
console.error('Failed to fetch database status:', error);
this.updateUI({
connected: false,
latency: 0,
writeCapable: false,
error: 'Failed to connect to server'
});
}
}
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;
}
start() {
// Initial check
this.checkStatus();
// 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;
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();
}
});

View File

@@ -156,6 +156,32 @@
</div> </div>
</main> </main>
<!-- Database Status Bar -->
<div class="db-status-bar" id="dbStatusBar">
<div class="db-status-item">
<span class="db-status-label">SQL Connection:</span>
<span class="db-status-indicator" id="dbConnection">
<span class="status-dot"></span>
<span class="status-text">Checking...</span>
</span>
</div>
<div class="db-status-item">
<span class="db-status-label">Latency:</span>
<span class="db-status-value" id="dbLatency">-- ms</span>
</div>
<div class="db-status-item">
<span class="db-status-label">Write Capability:</span>
<span class="db-status-indicator" id="dbWrite">
<span class="status-dot"></span>
<span class="status-text">Checking...</span>
</span>
</div>
<div class="db-status-item db-status-timestamp">
<span class="db-status-label">Last Check:</span>
<span class="db-status-value" id="dbTimestamp">--</span>
</div>
</div>
<footer class="footer"> <footer class="footer">
<p>© 2025 Beyond Cloud Technology. All rights reserved.</p> <p>© 2025 Beyond Cloud Technology. All rights reserved.</p>
</footer> </footer>
@@ -183,6 +209,7 @@
</div> </div>
<script src="storage.js"></script> <script src="storage.js"></script>
<script src="db-status.js"></script>
<script src="multiplayer.js"></script> <script src="multiplayer.js"></script>
<script src="game.js"></script> <script src="game.js"></script>
<script> <script>

View File

@@ -27,6 +27,75 @@ app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html')); res.sendFile(path.join(__dirname, 'index.html'));
}); });
// Database health check endpoint
app.get('/api/db-status', async (req, res) => {
const startTime = Date.now();
let status = {
connected: false,
latency: 0,
writeCapable: false,
timestamp: new Date().toISOString(),
error: null
};
try {
// Test connection with a simple query
const [result] = await db.pool.query('SELECT 1 as test');
const latency = Date.now() - startTime;
if (result && result[0].test === 1) {
status.connected = true;
status.latency = latency;
// Test write capability
try {
const testTableName = '_health_check_test';
// Create test table if it doesn't exist
await db.pool.query(`
CREATE TABLE IF NOT EXISTS ${testTableName} (
id INT AUTO_INCREMENT PRIMARY KEY,
test_value VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Try to insert a test record
const testValue = `test_${Date.now()}`;
await db.pool.query(
`INSERT INTO ${testTableName} (test_value) VALUES (?)`,
[testValue]
);
// Clean up old test records (keep only last 10)
await db.pool.query(`
DELETE FROM ${testTableName}
WHERE id NOT IN (
SELECT id FROM (
SELECT id FROM ${testTableName}
ORDER BY created_at DESC
LIMIT 10
) AS keep_records
)
`);
status.writeCapable = true;
} catch (writeError) {
console.error('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);
status.connected = false;
status.latency = Date.now() - startTime;
status.error = error.message;
}
res.json(status);
});
// Initialize game manager // Initialize game manager
const gameManager = new GameManager(io, db); const gameManager = new GameManager(io, db);

View File

@@ -528,6 +528,157 @@ body {
box-shadow: 0 6px 30px rgba(147, 51, 234, 0.6); box-shadow: 0 6px 30px rgba(147, 51, 234, 0.6);
} }
/* Database Status Bar */
.db-status-bar {
background: rgba(20, 24, 39, 0.95);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border: 1px solid var(--border-light);
border-radius: 12px;
padding: 1rem 1.5rem;
margin: 2rem 0 1rem;
display: flex;
flex-wrap: wrap;
gap: 2rem;
align-items: center;
justify-content: space-between;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
.db-status-bar:hover {
border-color: var(--border-medium);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
.db-status-item {
display: flex;
align-items: center;
gap: 0.75rem;
min-width: fit-content;
}
.db-status-label {
color: var(--text-secondary);
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.db-status-value {
color: var(--text-primary);
font-size: 0.95rem;
font-weight: 600;
font-family: 'Courier New', monospace;
padding: 0.25rem 0.75rem;
background: var(--bg-tertiary);
border-radius: 6px;
border: 1px solid var(--border-light);
transition: all 0.3s ease;
}
.db-status-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.75rem;
background: var(--bg-tertiary);
border-radius: 6px;
border: 1px solid var(--border-light);
transition: all 0.3s ease;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--text-muted);
transition: all 0.3s ease;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
.status-dot.status-connected {
background: var(--success);
box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
}
.status-dot.status-disconnected {
background: var(--danger);
box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
animation: pulse-error 1s ease-in-out infinite;
}
.status-dot.status-warning {
background: #f59e0b;
box-shadow: 0 0 10px rgba(245, 158, 11, 0.5);
}
@keyframes pulse-error {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.1);
}
}
.status-text {
color: var(--text-primary);
font-size: 0.9rem;
font-weight: 600;
}
.db-status-indicator.status-success {
border-color: rgba(16, 185, 129, 0.3);
background: rgba(16, 185, 129, 0.05);
}
.db-status-indicator.status-error {
border-color: rgba(239, 68, 68, 0.3);
background: rgba(239, 68, 68, 0.05);
}
.db-status-indicator.status-warning {
border-color: rgba(245, 158, 11, 0.3);
background: rgba(245, 158, 11, 0.05);
}
/* Latency color coding */
.latency-good {
color: var(--success);
border-color: rgba(16, 185, 129, 0.3);
background: rgba(16, 185, 129, 0.05);
}
.latency-ok {
color: #f59e0b;
border-color: rgba(245, 158, 11, 0.3);
background: rgba(245, 158, 11, 0.05);
}
.latency-slow {
color: var(--danger);
border-color: rgba(239, 68, 68, 0.3);
background: rgba(239, 68, 68, 0.05);
}
.db-status-timestamp {
margin-left: auto;
opacity: 0.7;
}
/* Footer */ /* Footer */
.footer { .footer {
margin-top: 2rem; margin-top: 2rem;
@@ -611,4 +762,19 @@ body {
.victory-title { .victory-title {
font-size: 2rem; font-size: 2rem;
} }
.db-status-bar {
flex-direction: column;
gap: 1rem;
padding: 1rem;
}
.db-status-item {
width: 100%;
justify-content: space-between;
}
.db-status-timestamp {
margin-left: 0;
}
} }