293 أسطر
9.4 KiB
HTML
293 أسطر
9.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>API Monitoring Dashboard</title>
|
|
<!-- Chart.js (lightweight, cached) -->
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: #f0f2f5;
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
h1 {
|
|
font-weight: 300;
|
|
color: #1a1a2e;
|
|
margin-bottom: 20px;
|
|
}
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.card {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
transition: 0.2s;
|
|
}
|
|
.card:hover {
|
|
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
|
}
|
|
.card-label {
|
|
font-size: 0.85rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: #6c757d;
|
|
margin-bottom: 6px;
|
|
}
|
|
.card-value {
|
|
font-size: 2rem;
|
|
font-weight: 600;
|
|
}
|
|
.status-up { color: #28a745; }
|
|
.status-down { color: #dc3545; }
|
|
.chart-container {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
margin-bottom: 20px;
|
|
height: 300px;
|
|
}
|
|
.table-container {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
overflow-x: auto;
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.9rem;
|
|
}
|
|
th {
|
|
text-align: left;
|
|
padding: 10px 8px;
|
|
border-bottom: 2px solid #dee2e6;
|
|
color: #495057;
|
|
}
|
|
td {
|
|
padding: 8px;
|
|
border-bottom: 1px solid #e9ecef;
|
|
}
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 2px 10px;
|
|
border-radius: 20px;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
}
|
|
.badge-up { background: #d4edda; color: #155724; }
|
|
.badge-down { background: #f8d7da; color: #721c24; }
|
|
.text-muted {
|
|
color: #6c757d;
|
|
font-size: 0.85rem;
|
|
}
|
|
@media (max-width: 600px) {
|
|
.grid {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
.card-value {
|
|
font-size: 1.5rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>📊 API Monitoring Dashboard</h1>
|
|
|
|
<!-- Summary Cards -->
|
|
<div class="grid" id="summary">
|
|
<div class="card">
|
|
<div class="card-label">Uptime (24h)</div>
|
|
<div class="card-value" id="uptime">--</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-label">Current Status</div>
|
|
<div class="card-value" id="status">--</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-label">Last Latency</div>
|
|
<div class="card-value" id="last-latency">--</div>
|
|
<div class="text-muted">milliseconds</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-label">Total Requests</div>
|
|
<div class="card-value" id="total-requests">--</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Latency Chart -->
|
|
<div class="chart-container">
|
|
<canvas id="latencyChart"></canvas>
|
|
</div>
|
|
|
|
<!-- Recent Checks Table -->
|
|
<div class="table-container">
|
|
<h3 style="margin-bottom: 10px; font-weight: 400;">Recent Checks (last 10)</h3>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Timestamp</th>
|
|
<th>Status</th>
|
|
<th>Latency (ms)</th>
|
|
<th>Request Count</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="checks-body">
|
|
<tr><td colspan="4" class="text-muted">Loading data…</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let chart = null;
|
|
|
|
// Compute uptime for the last 24 hours
|
|
function computeUptime(data) {
|
|
if (!data || data.length === 0) return 0;
|
|
const now = new Date();
|
|
const last24h = data.filter(entry => {
|
|
const ts = new Date(entry.timestamp);
|
|
return (now - ts) <= 24 * 60 * 60 * 1000;
|
|
});
|
|
if (last24h.length === 0) return 0;
|
|
const up = last24h.filter(e => e.status === 'up').length;
|
|
return (up / last24h.length) * 100;
|
|
}
|
|
|
|
// Update the dashboard with new data
|
|
function updateDashboard(data) {
|
|
if (!data || data.length === 0) {
|
|
document.getElementById('uptime').textContent = '--';
|
|
document.getElementById('status').innerHTML = '--';
|
|
document.getElementById('last-latency').textContent = '--';
|
|
document.getElementById('total-requests').textContent = '--';
|
|
document.getElementById('checks-body').innerHTML = '<tr><td colspan="4" class="text-muted">No data</td></tr>';
|
|
return;
|
|
}
|
|
|
|
// Latest entry
|
|
const latest = data[data.length - 1];
|
|
// Status with badge
|
|
const statusEl = document.getElementById('status');
|
|
if (latest.status === 'up') {
|
|
statusEl.innerHTML = '<span class="status-up">🟢 UP</span>';
|
|
} else {
|
|
statusEl.innerHTML = '<span class="status-down">🔴 DOWN</span>';
|
|
}
|
|
document.getElementById('last-latency').textContent = latest.latency >= 0 ? latest.latency : 'N/A';
|
|
document.getElementById('total-requests').textContent = latest.request_count || 0;
|
|
|
|
// Uptime
|
|
const uptime = computeUptime(data);
|
|
document.getElementById('uptime').textContent = uptime.toFixed(2) + '%';
|
|
|
|
// Update table (last 10, newest first)
|
|
const tbody = document.getElementById('checks-body');
|
|
tbody.innerHTML = '';
|
|
const last10 = data.slice(-10).reverse();
|
|
last10.forEach(entry => {
|
|
const row = document.createElement('tr');
|
|
const statusBadge = entry.status === 'up'
|
|
? '<span class="badge badge-up">UP</span>'
|
|
: '<span class="badge badge-down">DOWN</span>';
|
|
row.innerHTML = `
|
|
<td>${entry.timestamp}</td>
|
|
<td>${statusBadge}</td>
|
|
<td>${entry.latency >= 0 ? entry.latency : '—'}</td>
|
|
<td>${entry.request_count || '—'}</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
// Update chart (use last 60 entries, or all if less)
|
|
const chartData = data.slice(-60);
|
|
const labels = chartData.map(e => {
|
|
const d = new Date(e.timestamp);
|
|
return d.toLocaleTimeString();
|
|
});
|
|
const latencies = chartData.map(e => e.latency >= 0 ? e.latency : 0);
|
|
|
|
if (chart) {
|
|
chart.data.labels = labels;
|
|
chart.data.datasets[0].data = latencies;
|
|
chart.update();
|
|
} else {
|
|
const ctx = document.getElementById('latencyChart').getContext('2d');
|
|
chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Latency (ms)',
|
|
data: latencies,
|
|
borderColor: '#007bff',
|
|
backgroundColor: 'rgba(0,123,255,0.1)',
|
|
tension: 0.2,
|
|
fill: true,
|
|
pointRadius: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false }
|
|
},
|
|
scales: {
|
|
x: { grid: { display: false } },
|
|
y: { beginAtZero: true, grid: { color: '#f0f0f0' } }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Fetch the JSON data
|
|
async function fetchData() {
|
|
try {
|
|
const response = await fetch('monitoring-data.json');
|
|
if (!response.ok) throw new Error('Network error');
|
|
const data = await response.json();
|
|
// Ensure data is an array
|
|
if (!Array.isArray(data)) {
|
|
console.warn('Data is not an array; converting to empty array');
|
|
return [];
|
|
}
|
|
return data;
|
|
} catch (error) {
|
|
console.error('Failed to fetch data:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Refresh loop
|
|
async function refresh() {
|
|
const data = await fetchData();
|
|
updateDashboard(data);
|
|
}
|
|
|
|
// Initial load and periodic refresh every 10 seconds
|
|
refresh();
|
|
setInterval(refresh, 10000);
|
|
</script>
|
|
</body>
|
|
</html> |