|
|
<!DOCTYPE html>
|
|
|
<html lang="en">
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>Crypto Monitor - Complete Overview</title>
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
|
<style>
|
|
|
* {
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
body {
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
min-height: 100vh;
|
|
|
padding: 20px;
|
|
|
}
|
|
|
|
|
|
.container {
|
|
|
max-width: 1600px;
|
|
|
margin: 0 auto;
|
|
|
}
|
|
|
|
|
|
.header {
|
|
|
background: white;
|
|
|
border-radius: 15px;
|
|
|
padding: 30px;
|
|
|
margin-bottom: 20px;
|
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
|
|
}
|
|
|
|
|
|
.header h1 {
|
|
|
color: #667eea;
|
|
|
font-size: 2.5em;
|
|
|
margin-bottom: 10px;
|
|
|
}
|
|
|
|
|
|
.header p {
|
|
|
color: #666;
|
|
|
font-size: 1.1em;
|
|
|
}
|
|
|
|
|
|
.stats-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
|
gap: 20px;
|
|
|
margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
.stat-card {
|
|
|
background: white;
|
|
|
border-radius: 15px;
|
|
|
padding: 25px;
|
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
|
|
transition: transform 0.3s ease;
|
|
|
}
|
|
|
|
|
|
.stat-card:hover {
|
|
|
transform: translateY(-5px);
|
|
|
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
|
|
}
|
|
|
|
|
|
.stat-card h3 {
|
|
|
color: #999;
|
|
|
font-size: 0.9em;
|
|
|
text-transform: uppercase;
|
|
|
margin-bottom: 10px;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.stat-card .value {
|
|
|
font-size: 2.5em;
|
|
|
font-weight: bold;
|
|
|
margin-bottom: 5px;
|
|
|
}
|
|
|
|
|
|
.stat-card .label {
|
|
|
color: #666;
|
|
|
font-size: 0.9em;
|
|
|
}
|
|
|
|
|
|
.stat-card.green .value { color: #10b981; }
|
|
|
.stat-card.blue .value { color: #3b82f6; }
|
|
|
.stat-card.purple .value { color: #8b5cf6; }
|
|
|
.stat-card.orange .value { color: #f59e0b; }
|
|
|
.stat-card.red .value { color: #ef4444; }
|
|
|
|
|
|
.main-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: 2fr 1fr;
|
|
|
gap: 20px;
|
|
|
margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
.card {
|
|
|
background: white;
|
|
|
border-radius: 15px;
|
|
|
padding: 25px;
|
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
|
|
}
|
|
|
|
|
|
.card h2 {
|
|
|
color: #333;
|
|
|
margin-bottom: 20px;
|
|
|
font-size: 1.5em;
|
|
|
border-bottom: 3px solid #667eea;
|
|
|
padding-bottom: 10px;
|
|
|
}
|
|
|
|
|
|
.providers-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
|
gap: 15px;
|
|
|
max-height: 500px;
|
|
|
overflow-y: auto;
|
|
|
}
|
|
|
|
|
|
.provider-card {
|
|
|
background: #f8f9fa;
|
|
|
border-radius: 10px;
|
|
|
padding: 15px;
|
|
|
border-left: 4px solid #ddd;
|
|
|
transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
.provider-card:hover {
|
|
|
transform: translateX(5px);
|
|
|
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
|
|
}
|
|
|
|
|
|
.provider-card.online { border-left-color: #10b981; background: #f0fdf4; }
|
|
|
.provider-card.offline { border-left-color: #ef4444; background: #fef2f2; }
|
|
|
.provider-card.degraded { border-left-color: #f59e0b; background: #fffbeb; }
|
|
|
|
|
|
.provider-card .name {
|
|
|
font-weight: 600;
|
|
|
color: #333;
|
|
|
margin-bottom: 5px;
|
|
|
}
|
|
|
|
|
|
.provider-card .category {
|
|
|
font-size: 0.85em;
|
|
|
color: #666;
|
|
|
margin-bottom: 5px;
|
|
|
}
|
|
|
|
|
|
.provider-card .status {
|
|
|
font-size: 0.8em;
|
|
|
padding: 3px 8px;
|
|
|
border-radius: 5px;
|
|
|
display: inline-block;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.status.online { background: #10b981; color: white; }
|
|
|
.status.offline { background: #ef4444; color: white; }
|
|
|
.status.degraded { background: #f59e0b; color: white; }
|
|
|
|
|
|
.category-list {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 15px;
|
|
|
}
|
|
|
|
|
|
.category-item {
|
|
|
background: #f8f9fa;
|
|
|
border-radius: 10px;
|
|
|
padding: 15px;
|
|
|
border-left: 4px solid #667eea;
|
|
|
}
|
|
|
|
|
|
.category-item .cat-name {
|
|
|
font-weight: 600;
|
|
|
color: #333;
|
|
|
margin-bottom: 10px;
|
|
|
font-size: 1.1em;
|
|
|
}
|
|
|
|
|
|
.category-item .cat-stats {
|
|
|
display: flex;
|
|
|
gap: 15px;
|
|
|
font-size: 0.9em;
|
|
|
}
|
|
|
|
|
|
.category-item .cat-stat {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 5px;
|
|
|
}
|
|
|
|
|
|
.chart-container {
|
|
|
position: relative;
|
|
|
height: 300px;
|
|
|
margin-top: 20px;
|
|
|
}
|
|
|
|
|
|
.loading {
|
|
|
text-align: center;
|
|
|
padding: 40px;
|
|
|
color: #666;
|
|
|
font-size: 1.2em;
|
|
|
}
|
|
|
|
|
|
.refresh-btn {
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
color: white;
|
|
|
border: none;
|
|
|
padding: 12px 30px;
|
|
|
border-radius: 10px;
|
|
|
font-size: 1em;
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.3s ease;
|
|
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
|
|
}
|
|
|
|
|
|
.refresh-btn:hover {
|
|
|
transform: translateY(-2px);
|
|
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
|
|
}
|
|
|
|
|
|
.refresh-btn:active {
|
|
|
transform: translateY(0);
|
|
|
}
|
|
|
|
|
|
@keyframes pulse {
|
|
|
0%, 100% { opacity: 1; }
|
|
|
50% { opacity: 0.5; }
|
|
|
}
|
|
|
|
|
|
.updating {
|
|
|
animation: pulse 1.5s ease-in-out infinite;
|
|
|
}
|
|
|
|
|
|
.full-width {
|
|
|
grid-column: 1 / -1;
|
|
|
}
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
.main-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
.stats-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="container">
|
|
|
<div class="header">
|
|
|
<h1>🚀 Crypto API Monitor</h1>
|
|
|
<p>Complete Real-Time Overview of All Cryptocurrency Data Sources</p>
|
|
|
<button class="refresh-btn" onclick="refreshAll()">🔄 Refresh Data</button>
|
|
|
</div>
|
|
|
|
|
|
<div class="stats-grid">
|
|
|
<div class="stat-card green">
|
|
|
<h3>Total Providers</h3>
|
|
|
<div class="value" id="totalProviders">-</div>
|
|
|
<div class="label">API Sources</div>
|
|
|
</div>
|
|
|
<div class="stat-card blue">
|
|
|
<h3>Online</h3>
|
|
|
<div class="value" id="onlineProviders">-</div>
|
|
|
<div class="label">Active & Working</div>
|
|
|
</div>
|
|
|
<div class="stat-card orange">
|
|
|
<h3>Degraded</h3>
|
|
|
<div class="value" id="degradedProviders">-</div>
|
|
|
<div class="label">Slow Response</div>
|
|
|
</div>
|
|
|
<div class="stat-card red">
|
|
|
<h3>Offline</h3>
|
|
|
<div class="value" id="offlineProviders">-</div>
|
|
|
<div class="label">Not Responding</div>
|
|
|
</div>
|
|
|
<div class="stat-card purple">
|
|
|
<h3>Categories</h3>
|
|
|
<div class="value" id="totalCategories">-</div>
|
|
|
<div class="label">Data Types</div>
|
|
|
</div>
|
|
|
<div class="stat-card green">
|
|
|
<h3>Uptime</h3>
|
|
|
<div class="value" id="uptimePercent">-</div>
|
|
|
<div class="label">Overall Health</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="main-grid">
|
|
|
<div class="card">
|
|
|
<h2>📊 All Providers Status</h2>
|
|
|
<div class="providers-grid" id="providersGrid">
|
|
|
<div class="loading">Loading providers...</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="card">
|
|
|
<h2>📁 Categories</h2>
|
|
|
<div class="category-list" id="categoryList">
|
|
|
<div class="loading">Loading categories...</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="card full-width">
|
|
|
<h2>📈 Status Distribution</h2>
|
|
|
<div class="chart-container">
|
|
|
<canvas id="statusChart"></canvas>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
let statusChart = null;
|
|
|
|
|
|
async function loadProviders() {
|
|
|
try {
|
|
|
const response = await fetch('/api/providers');
|
|
|
const providers = await response.json();
|
|
|
|
|
|
|
|
|
const online = providers.filter(p => p.status === 'online').length;
|
|
|
const offline = providers.filter(p => p.status === 'offline').length;
|
|
|
const degraded = providers.filter(p => p.status === 'degraded').length;
|
|
|
const uptime = ((online / providers.length) * 100).toFixed(1);
|
|
|
|
|
|
document.getElementById('totalProviders').textContent = providers.length;
|
|
|
document.getElementById('onlineProviders').textContent = online;
|
|
|
document.getElementById('degradedProviders').textContent = degraded;
|
|
|
document.getElementById('offlineProviders').textContent = offline;
|
|
|
document.getElementById('uptimePercent').textContent = uptime + '%';
|
|
|
|
|
|
|
|
|
const categories = {};
|
|
|
providers.forEach(p => {
|
|
|
if (!categories[p.category]) {
|
|
|
categories[p.category] = { total: 0, online: 0, offline: 0, degraded: 0 };
|
|
|
}
|
|
|
categories[p.category].total++;
|
|
|
categories[p.category][p.status]++;
|
|
|
});
|
|
|
|
|
|
document.getElementById('totalCategories').textContent = Object.keys(categories).length;
|
|
|
|
|
|
|
|
|
const providersGrid = document.getElementById('providersGrid');
|
|
|
providersGrid.innerHTML = providers.map(p => `
|
|
|
<div class="provider-card ${p.status}">
|
|
|
<div class="name">${p.name}</div>
|
|
|
<div class="category">${p.category}</div>
|
|
|
<span class="status ${p.status}">${p.status.toUpperCase()}</span>
|
|
|
${p.response_time_ms ? `<div style="font-size: 0.8em; color: #666; margin-top: 5px;">${Math.round(p.response_time_ms)}ms</div>` : ''}
|
|
|
</div>
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
const categoryList = document.getElementById('categoryList');
|
|
|
categoryList.innerHTML = Object.entries(categories).map(([name, stats]) => `
|
|
|
<div class="category-item">
|
|
|
<div class="cat-name">${name}</div>
|
|
|
<div class="cat-stats">
|
|
|
<div class="cat-stat">
|
|
|
<span style="color: #10b981;">●</span>
|
|
|
<span>${stats.online} online</span>
|
|
|
</div>
|
|
|
<div class="cat-stat">
|
|
|
<span style="color: #f59e0b;">●</span>
|
|
|
<span>${stats.degraded} degraded</span>
|
|
|
</div>
|
|
|
<div class="cat-stat">
|
|
|
<span style="color: #ef4444;">●</span>
|
|
|
<span>${stats.offline} offline</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
updateChart(online, degraded, offline);
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('Error loading providers:', error);
|
|
|
document.getElementById('providersGrid').innerHTML = '<div class="loading">Error loading data</div>';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function updateChart(online, degraded, offline) {
|
|
|
const ctx = document.getElementById('statusChart').getContext('2d');
|
|
|
|
|
|
if (statusChart) {
|
|
|
statusChart.destroy();
|
|
|
}
|
|
|
|
|
|
statusChart = new Chart(ctx, {
|
|
|
type: 'doughnut',
|
|
|
data: {
|
|
|
labels: ['Online', 'Degraded', 'Offline'],
|
|
|
datasets: [{
|
|
|
data: [online, degraded, offline],
|
|
|
backgroundColor: ['#10b981', '#f59e0b', '#ef4444'],
|
|
|
borderWidth: 0
|
|
|
}]
|
|
|
},
|
|
|
options: {
|
|
|
responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
|
plugins: {
|
|
|
legend: {
|
|
|
position: 'bottom',
|
|
|
labels: {
|
|
|
font: { size: 14 },
|
|
|
padding: 20
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
async function refreshAll() {
|
|
|
const btn = document.querySelector('.refresh-btn');
|
|
|
btn.classList.add('updating');
|
|
|
btn.textContent = '⏳ Refreshing...';
|
|
|
|
|
|
await loadProviders();
|
|
|
|
|
|
btn.classList.remove('updating');
|
|
|
btn.textContent = '🔄 Refresh Data';
|
|
|
}
|
|
|
|
|
|
|
|
|
loadProviders();
|
|
|
|
|
|
|
|
|
setInterval(loadProviders, 30000);
|
|
|
</script>
|
|
|
</body>
|
|
|
</html>
|
|
|
|
|
|
|