-
Notifications
You must be signed in to change notification settings - Fork 20
Feat/health dashboard #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| /* Health Dashboard Styles */ | ||
| * { margin: 0; padding: 0; box-sizing: border-box; } | ||
|
|
||
| body { | ||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | ||
| background: #f0f2f5; | ||
| min-height: 100vh; | ||
| } | ||
|
|
||
| .dashboard { | ||
| max-width: 1400px; | ||
| margin: 0 auto; | ||
| padding: 2rem; | ||
| } | ||
|
|
||
| header { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| margin-bottom: 2rem; | ||
| } | ||
|
|
||
| h1 { color: #1a1a2e; } | ||
|
|
||
| .status-badge { | ||
| padding: 0.5rem 1.5rem; | ||
| border-radius: 20px; | ||
| font-weight: 600; | ||
| font-size: 0.9rem; | ||
| } | ||
|
|
||
| .status-badge.healthy { background: #10b981; color: white; } | ||
| .status-badge.warning { background: #f59e0b; color: white; } | ||
| .status-badge.critical { background: #ef4444; color: white; } | ||
|
|
||
| .metrics-grid { | ||
| display: grid; | ||
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | ||
| gap: 1.5rem; | ||
| margin-bottom: 2rem; | ||
| } | ||
|
|
||
| .metric-card { | ||
| background: white; | ||
| border-radius: 12px; | ||
| padding: 1.5rem; | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 1rem; | ||
| box-shadow: 0 2px 8px rgba(0,0,0,0.05); | ||
| } | ||
|
|
||
| .metric-icon { font-size: 2.5rem; } | ||
|
|
||
| .metric-content h3 { | ||
| font-size: 0.85rem; | ||
| color: #666; | ||
| margin-bottom: 0.25rem; | ||
| } | ||
|
|
||
| .metric-value { | ||
| font-size: 2rem; | ||
| font-weight: 700; | ||
| color: #1a1a2e; | ||
| } | ||
|
|
||
| .metric-label { | ||
| font-size: 0.75rem; | ||
| color: #999; | ||
| } | ||
|
|
||
| .services-section { | ||
| background: white; | ||
| border-radius: 12px; | ||
| padding: 1.5rem; | ||
| margin-bottom: 2rem; | ||
| box-shadow: 0 2px 8px rgba(0,0,0,0.05); | ||
| } | ||
|
|
||
| .services-section h2 { | ||
| margin-bottom: 1rem; | ||
| color: #1a1a2e; | ||
| } | ||
|
|
||
| .services-list { display: grid; gap: 1rem; } | ||
|
|
||
| .service-item { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| padding: 1rem; | ||
| background: #f8f9fa; | ||
| border-radius: 8px; | ||
| } | ||
|
|
||
| .service-name { font-weight: 600; } | ||
|
|
||
| .service-status { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 0.5rem; | ||
| } | ||
|
|
||
| .status-dot { | ||
| width: 10px; | ||
| height: 10px; | ||
| border-radius: 50%; | ||
| } | ||
|
|
||
| .status-dot.up { background: #10b981; } | ||
| .status-dot.down { background: #ef4444; } | ||
|
|
||
| .chart-section { | ||
| background: white; | ||
| border-radius: 12px; | ||
| padding: 1.5rem; | ||
| box-shadow: 0 2px 8px rgba(0,0,0,0.05); | ||
| } | ||
|
|
||
| .chart-section h2 { | ||
| margin-bottom: 1rem; | ||
| color: #1a1a2e; | ||
| } | ||
|
|
||
| #trendChart { | ||
| width: 100%; | ||
| height: 200px; | ||
| } | ||
|
|
||
| footer { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| margin-top: 2rem; | ||
| color: #666; | ||
| } | ||
|
|
||
| button { | ||
| padding: 0.5rem 1rem; | ||
| background: #3b82f6; | ||
| color: white; | ||
| border: none; | ||
| border-radius: 6px; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| button:hover { background: #2563eb; } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| // Health Dashboard Logic | ||
|
|
||
| const services = [ | ||
| { name: 'API Gateway', endpoint: '/health/gateway' }, | ||
| { name: 'Auth Service', endpoint: '/health/auth' }, | ||
| { name: 'Database', endpoint: '/health/db' }, | ||
| { name: 'Cache', endpoint: '/health/cache' }, | ||
| { name: 'Queue', endpoint: '/health/queue' }, | ||
| ]; | ||
|
|
||
| const metrics = { | ||
| responseTime: [], | ||
| requests: 0, | ||
| errors: 0, | ||
| success: 0, | ||
| }; | ||
|
|
||
| async function fetchHealth() { | ||
| try { | ||
| const response = await fetch('/api/health'); | ||
| const data = await response.json(); | ||
| return data; | ||
| } catch (error) { | ||
| console.error('Health check failed:', error); | ||
| return null; | ||
| } | ||
| } | ||
|
Comment on lines
+18
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don’t mark stale data as fresh.
Suggested fix async function fetchHealth() {
try {
const response = await fetch('/api/health');
+ if (!response.ok) {
+ throw new Error(`Health check failed: ${response.status}`);
+ }
const data = await response.json();
return data;
} catch (error) {
console.error('Health check failed:', error);
return null;
@@
async function refreshData() {
const health = await fetchHealth();
-
- if (health) {
- updateMetrics(health);
- updateServices(health.services);
- updateOverallStatus(health.status);
- }
-
- document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
+ if (!health) return;
+
+ updateMetrics(health);
+ updateServices(health.services);
+ updateOverallStatus(health.status);
+ document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
}Also applies to: 29-39 |
||
|
|
||
| async function refreshData() { | ||
| const health = await fetchHealth(); | ||
|
|
||
| if (health) { | ||
| updateMetrics(health); | ||
| updateServices(health.services); | ||
| updateOverallStatus(health.status); | ||
| } | ||
|
|
||
| document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString(); | ||
| } | ||
|
Comment on lines
+29
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The trend chart is still demo data.
Suggested fix function updateMetrics(data) {
+ if (typeof data.avgResponseTime === 'number') {
+ metrics.responseTime = [...metrics.responseTime.slice(-29), data.avgResponseTime];
+ }
document.getElementById('responseTime').textContent = data.avgResponseTime || '--';
@@
updateMetrics(health);
updateServices(health.services);
updateOverallStatus(health.status);
+ drawChart(metrics.responseTime);
document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
}
@@
-function drawChart() {
+function drawChart(data = metrics.responseTime) {
const canvas = document.getElementById('trendChart');
const ctx = canvas.getContext('2d');
@@
- // Sample data
- const data = [120, 115, 130, 125, 140, 118, 122, 135, 128, 142];
+ if (!data.length) {
+ ctx.clearRect(0, 0, width, height);
+ return;
+ }Also applies to: 41-46, 82-131, 134-138 Prevent overlapping refreshes from winning out of order. The 30s timer here plus the manual refresh button in Suggested fix+let refreshToken = 0;
+
async function refreshData() {
+ const token = ++refreshToken;
const health = await fetchHealth();
- if (!health) return;
+ if (!health || token !== refreshToken) return;
updateMetrics(health);
updateServices(health.services);
updateOverallStatus(health.status);
document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
}Also applies to: 134-138 |
||
|
|
||
| function updateMetrics(data) { | ||
| document.getElementById('responseTime').textContent = data.avgResponseTime || '--'; | ||
| document.getElementById('successRate').textContent = data.successRate || '--'; | ||
| document.getElementById('requestRate').textContent = data.requestRate || '--'; | ||
| document.getElementById('errorCount').textContent = data.errorCount || '--'; | ||
| } | ||
|
Comment on lines
+41
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don’t treat zero as missing.
Suggested fix- document.getElementById('responseTime').textContent = data.avgResponseTime || '--';
- document.getElementById('successRate').textContent = data.successRate || '--';
- document.getElementById('requestRate').textContent = data.requestRate || '--';
- document.getElementById('errorCount').textContent = data.errorCount || '--';
+ document.getElementById('responseTime').textContent = data.avgResponseTime ?? '--';
+ document.getElementById('successRate').textContent = data.successRate ?? '--';
+ document.getElementById('requestRate').textContent = data.requestRate ?? '--';
+ document.getElementById('errorCount').textContent = data.errorCount ?? '--'; |
||
|
|
||
| function updateServices(servicesData) { | ||
| const list = document.getElementById('servicesList'); | ||
| list.innerHTML = services.map(service => { | ||
| const status = servicesData?.[service.name] || 'unknown'; | ||
| const isUp = status === 'healthy' || status === 'up'; | ||
| return ` | ||
| <div class="service-item"> | ||
| <span class="service-name">${service.name}</span> | ||
| <div class="service-status"> | ||
| <span class="status-dot ${isUp ? 'up' : 'down'}"></span> | ||
| <span>${isUp ? 'Operational' : 'Down'}</span> | ||
| </div> | ||
| </div> | ||
| `; | ||
| }).join(''); | ||
| } | ||
|
|
||
| function updateOverallStatus(status) { | ||
| const badge = document.getElementById('overallStatus'); | ||
| badge.className = 'status-badge'; | ||
|
|
||
| if (status === 'healthy') { | ||
| badge.classList.add('healthy'); | ||
| badge.textContent = '✅ All Systems Operational'; | ||
| } else if (status === 'warning') { | ||
| badge.classList.add('warning'); | ||
| badge.textContent = '⚠️ Partial Outage'; | ||
| } else { | ||
| badge.classList.add('critical'); | ||
| badge.textContent = '❌ Major Outage'; | ||
| } | ||
| } | ||
|
|
||
| // Simple chart | ||
| function drawChart() { | ||
| const canvas = document.getElementById('trendChart'); | ||
| const ctx = canvas.getContext('2d'); | ||
|
|
||
| canvas.width = canvas.offsetWidth; | ||
| canvas.height = canvas.offsetHeight; | ||
|
|
||
| // Sample data | ||
| const data = [120, 115, 130, 125, 140, 118, 122, 135, 128, 142]; | ||
| const width = canvas.width; | ||
| const height = canvas.height; | ||
|
|
||
| ctx.clearRect(0, 0, width, height); | ||
|
|
||
| // Draw grid | ||
| ctx.strokeStyle = '#e5e7eb'; | ||
| ctx.lineWidth = 1; | ||
| for (let i = 0; i < 5; i++) { | ||
| const y = (height / 5) * i; | ||
| ctx.beginPath(); | ||
| ctx.moveTo(0, y); | ||
| ctx.lineTo(width, y); | ||
| ctx.stroke(); | ||
| } | ||
|
|
||
| // Draw line | ||
| const maxVal = Math.max(...data); | ||
| const step = width / (data.length - 1); | ||
|
|
||
| ctx.strokeStyle = '#3b82f6'; | ||
| ctx.lineWidth = 2; | ||
| ctx.beginPath(); | ||
|
|
||
| data.forEach((val, i) => { | ||
| const x = i * step; | ||
| const y = height - (val / maxVal) * height * 0.8; | ||
|
|
||
| if (i === 0) ctx.moveTo(x, y); | ||
| else ctx.lineTo(x, y); | ||
| }); | ||
|
|
||
| ctx.stroke(); | ||
|
|
||
| // Fill area | ||
| ctx.lineTo(width, height); | ||
| ctx.lineTo(0, height); | ||
| ctx.closePath(); | ||
| ctx.fillStyle = 'rgba(59, 130, 246, 0.1)'; | ||
| ctx.fill(); | ||
| } | ||
|
|
||
| // Initialize | ||
| document.addEventListener('DOMContentLoaded', () => { | ||
| refreshData(); | ||
| drawChart(); | ||
| setInterval(refreshData, 30000); // Refresh every 30s | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>System Health Dashboard</title> | ||
| <link rel="stylesheet" href="dashboard.css"> | ||
| </head> | ||
| <body> | ||
| <div class="dashboard"> | ||
| <header> | ||
| <h1>🏥 Health Dashboard</h1> | ||
| <div class="status-badge" id="overallStatus">Checking...</div> | ||
| </header> | ||
|
|
||
| <main> | ||
| <section class="metrics-grid"> | ||
| <div class="metric-card"> | ||
| <div class="metric-icon">⚡</div> | ||
| <div class="metric-content"> | ||
| <h3>API Response Time</h3> | ||
| <div class="metric-value" id="responseTime">--</div> | ||
| <div class="metric-label">ms average</div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="metric-card"> | ||
| <div class="metric-icon">✅</div> | ||
| <div class="metric-content"> | ||
| <h3>Success Rate</h3> | ||
| <div class="metric-value" id="successRate">--</div> | ||
| <div class="metric-label">% requests</div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="metric-card"> | ||
| <div class="metric-icon">🔄</div> | ||
| <div class="metric-content"> | ||
| <h3>Requests/min</h3> | ||
| <div class="metric-value" id="requestRate">--</div> | ||
| <div class="metric-label">current load</div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="metric-card"> | ||
| <div class="metric-icon">⚠️</div> | ||
| <div class="metric-content"> | ||
| <h3>Error Count</h3> | ||
| <div class="metric-value" id="errorCount">--</div> | ||
| <div class="metric-label">last hour</div> | ||
| </div> | ||
| </div> | ||
| </section> | ||
|
|
||
| <section class="services-section"> | ||
| <h2>Service Status</h2> | ||
| <div class="services-list" id="servicesList"> | ||
| <!-- Dynamically populated --> | ||
| </div> | ||
| </section> | ||
|
|
||
| <section class="chart-section"> | ||
| <h2>Response Time Trend</h2> | ||
| <canvas id="trendChart"></canvas> | ||
| </section> | ||
| </main> | ||
|
|
||
| <footer> | ||
| <span>Last updated: <span id="lastUpdated">--</span></span> | ||
| <button onclick="refreshData()">🔄 Refresh</button> | ||
| </footer> | ||
| </div> | ||
|
|
||
| <script src="dashboard.js"></script> | ||
| </body> | ||
| </html> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Status badge contrast misses AA.
The
healthy,warning, andcriticalbadges use white text at0.9remon bright fills, so the dashboard’s primary status indicator is hard to read.Suggested fix
📝 Committable suggestion