| <!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>
|
|
|
|
|