Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions dashboard/dashboard.css
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; }
Comment on lines +25 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Status badge contrast misses AA.

The healthy, warning, and critical badges use white text at 0.9rem on bright fills, so the dashboard’s primary status indicator is hard to read.

Suggested fix
-.status-badge.healthy { background: `#10b981`; color: white; }
-.status-badge.warning { background: `#f59e0b`; color: white; }
-.status-badge.critical { background: `#ef4444`; color: white; }
+.status-badge.healthy { background: `#047857`; color: white; }
+.status-badge.warning { background: `#b45309`; color: white; }
+.status-badge.critical { background: `#b91c1c`; color: white; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.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; }
.status-badge {
padding: 0.5rem 1.5rem;
border-radius: 20px;
font-weight: 600;
font-size: 0.9rem;
}
.status-badge.healthy { background: `#047857`; color: white; }
.status-badge.warning { background: `#b45309`; color: white; }
.status-badge.critical { background: `#b91c1c`; 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; }
138 changes: 138 additions & 0 deletions dashboard/dashboard.js
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t mark stale data as fresh.

fetch() only rejects on network failures. A 500/503 still reaches response.json(), and lastUpdated is written even when no new state was applied. During an outage, the page can show old metrics with a current timestamp.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The trend chart is still demo data.

drawChart() renders a hard-coded array once on load. The metric card uses /api/health, but the chart never records avgResponseTime or redraws after refresh, so the two views can disagree.

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


⚠️ Potential issue | 🟠 Major

Prevent overlapping refreshes from winning out of order.

The 30s timer here plus the manual refresh button in dashboard/index.html can start another fetch before the first one finishes. A slower, older response can then land last and overwrite newer health data.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t treat zero as missing.

errorCount: 0 and requestRate: 0 render as -- here. On a health dashboard, that’s wrong telemetry.

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
});
76 changes: 76 additions & 0 deletions dashboard/index.html
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>
Loading