Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Crypto API Monitor - Complete Dashboard</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: #0f172a; | |
| color: #e2e8f0; | |
| overflow-x: hidden; | |
| } | |
| .header { | |
| background: linear-gradient(135deg, #1e293b 0%, #334155 100%); | |
| padding: 20px 40px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.3); | |
| position: sticky; | |
| top: 0; | |
| z-index: 1000; | |
| } | |
| .header-content { | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .logo { | |
| font-size: 24px; | |
| font-weight: bold; | |
| background: linear-gradient(135deg, #3b82f6, #8b5cf6); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .header-actions { | |
| display: flex; | |
| gap: 15px; | |
| align-items: center; | |
| } | |
| .btn { | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-size: 14px; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, #3b82f6, #8b5cf6); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4); | |
| } | |
| .btn-secondary { | |
| background: #334155; | |
| color: #e2e8f0; | |
| } | |
| .btn-secondary:hover { | |
| background: #475569; | |
| } | |
| .container { | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| padding: 30px; | |
| } | |
| .tabs { | |
| display: flex; | |
| gap: 5px; | |
| margin-bottom: 30px; | |
| background: #1e293b; | |
| padding: 10px; | |
| border-radius: 12px; | |
| overflow-x: auto; | |
| } | |
| .tab { | |
| padding: 12px 24px; | |
| border: none; | |
| background: transparent; | |
| color: #94a3b8; | |
| cursor: pointer; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| white-space: nowrap; | |
| } | |
| .tab:hover { | |
| background: #334155; | |
| color: #e2e8f0; | |
| } | |
| .tab.active { | |
| background: linear-gradient(135deg, #3b82f6, #8b5cf6); | |
| color: white; | |
| } | |
| .tab-content { | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| } | |
| .stat-card { | |
| background: linear-gradient(135deg, #1e293b 0%, #334155 100%); | |
| padding: 25px; | |
| border-radius: 12px; | |
| border: 1px solid #334155; | |
| transition: all 0.3s ease; | |
| } | |
| .stat-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3); | |
| border-color: #3b82f6; | |
| } | |
| .stat-card h3 { | |
| color: #94a3b8; | |
| font-size: 14px; | |
| text-transform: uppercase; | |
| margin-bottom: 10px; | |
| font-weight: 600; | |
| } | |
| .stat-card .value { | |
| font-size: 36px; | |
| font-weight: bold; | |
| margin-bottom: 5px; | |
| } | |
| .stat-card .label { | |
| color: #64748b; | |
| font-size: 13px; | |
| } | |
| .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; } | |
| .card { | |
| background: linear-gradient(135deg, #1e293b 0%, #334155 100%); | |
| border-radius: 12px; | |
| padding: 25px; | |
| border: 1px solid #334155; | |
| margin-bottom: 20px; | |
| } | |
| .card h2 { | |
| color: #e2e8f0; | |
| margin-bottom: 20px; | |
| font-size: 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .providers-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| gap: 15px; | |
| max-height: 600px; | |
| overflow-y: auto; | |
| } | |
| .provider-card { | |
| background: #0f172a; | |
| border-radius: 8px; | |
| padding: 15px; | |
| border-left: 4px solid #334155; | |
| transition: all 0.3s ease; | |
| } | |
| .provider-card:hover { | |
| transform: translateX(5px); | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.3); | |
| } | |
| .provider-card.online { | |
| border-left-color: #10b981; | |
| background: linear-gradient(to right, rgba(16, 185, 129, 0.1), #0f172a); | |
| } | |
| .provider-card.offline { | |
| border-left-color: #ef4444; | |
| background: linear-gradient(to right, rgba(239, 68, 68, 0.1), #0f172a); | |
| } | |
| .provider-card.degraded { | |
| border-left-color: #f59e0b; | |
| background: linear-gradient(to right, rgba(245, 158, 11, 0.1), #0f172a); | |
| } | |
| .provider-card .name { | |
| font-weight: 600; | |
| color: #e2e8f0; | |
| margin-bottom: 8px; | |
| font-size: 15px; | |
| } | |
| .provider-card .category { | |
| font-size: 12px; | |
| color: #94a3b8; | |
| margin-bottom: 8px; | |
| } | |
| .provider-card .status { | |
| display: inline-block; | |
| padding: 4px 10px; | |
| border-radius: 6px; | |
| font-size: 11px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| } | |
| .status.online { background: rgba(16, 185, 129, 0.2); color: #10b981; } | |
| .status.offline { background: rgba(239, 68, 68, 0.2); color: #ef4444; } | |
| .status.degraded { background: rgba(245, 158, 11, 0.2); color: #f59e0b; } | |
| .provider-card .response-time { | |
| font-size: 12px; | |
| color: #64748b; | |
| margin-top: 8px; | |
| } | |
| .category-list { | |
| display: grid; | |
| gap: 15px; | |
| } | |
| .category-item { | |
| background: #0f172a; | |
| border-radius: 8px; | |
| padding: 20px; | |
| border-left: 4px solid #3b82f6; | |
| } | |
| .category-item .name { | |
| font-weight: 600; | |
| color: #e2e8f0; | |
| margin-bottom: 12px; | |
| font-size: 16px; | |
| } | |
| .category-item .stats { | |
| display: flex; | |
| gap: 20px; | |
| font-size: 14px; | |
| } | |
| .category-item .stat { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .loading { | |
| text-align: center; | |
| padding: 60px; | |
| color: #64748b; | |
| font-size: 16px; | |
| } | |
| .spinner { | |
| border: 3px solid #334155; | |
| border-top: 3px solid #3b82f6; | |
| border-radius: 50%; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| margin: 20px auto; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .grid-2 { | |
| display: grid; | |
| grid-template-columns: 2fr 1fr; | |
| gap: 20px; | |
| } | |
| .market-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 15px; | |
| background: #0f172a; | |
| border-radius: 8px; | |
| margin-bottom: 10px; | |
| border: 1px solid #334155; | |
| } | |
| .market-item:hover { | |
| border-color: #3b82f6; | |
| } | |
| .market-item .coin-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| } | |
| .market-item .coin-name { | |
| font-weight: 600; | |
| color: #e2e8f0; | |
| } | |
| .market-item .coin-symbol { | |
| color: #94a3b8; | |
| font-size: 14px; | |
| } | |
| .market-item .price { | |
| font-size: 18px; | |
| font-weight: 600; | |
| color: #3b82f6; | |
| } | |
| .market-item .change { | |
| padding: 4px 10px; | |
| border-radius: 6px; | |
| font-weight: 600; | |
| font-size: 14px; | |
| } | |
| .market-item .change.positive { | |
| background: rgba(16, 185, 129, 0.2); | |
| color: #10b981; | |
| } | |
| .market-item .change.negative { | |
| background: rgba(239, 68, 68, 0.2); | |
| color: #ef4444; | |
| } | |
| .search-box { | |
| width: 100%; | |
| padding: 12px 20px; | |
| background: #0f172a; | |
| border: 1px solid #334155; | |
| border-radius: 8px; | |
| color: #e2e8f0; | |
| font-size: 14px; | |
| margin-bottom: 20px; | |
| } | |
| .search-box:focus { | |
| outline: none; | |
| border-color: #3b82f6; | |
| } | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #1e293b; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #334155; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #475569; | |
| } | |
| @media (max-width: 768px) { | |
| .grid-2 { | |
| grid-template-columns: 1fr; | |
| } | |
| .stats-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .header-content { | |
| flex-direction: column; | |
| gap: 15px; | |
| } | |
| } | |
| .status-indicator { | |
| display: inline-block; | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| margin-right: 8px; | |
| } | |
| .status-indicator.online { background: #10b981; } | |
| .status-indicator.offline { background: #ef4444; } | |
| .status-indicator.degraded { background: #f59e0b; } | |
| .empty-state { | |
| text-align: center; | |
| padding: 60px 20px; | |
| color: #64748b; | |
| } | |
| .empty-state h3 { | |
| color: #94a3b8; | |
| margin-bottom: 10px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"> | |
| <div class="header-content"> | |
| <div class="logo"> | |
| <span>🚀</span> | |
| <span>Crypto API Monitor</span> | |
| </div> | |
| <div class="header-actions"> | |
| <span id="connectionStatus" style="color: #64748b; font-size: 14px;"> | |
| <span class="status-indicator online"></span> | |
| Connected | |
| </span> | |
| <button class="btn btn-secondary" onclick="refreshData()">🔄 Refresh</button> | |
| <button class="btn btn-primary" onclick="exportData()">💾 Export</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="container"> | |
| <div class="tabs"> | |
| <button class="tab active" onclick="switchTab('overview')">📊 Overview</button> | |
| <button class="tab" onclick="switchTab('providers')">🔌 Providers</button> | |
| <button class="tab" onclick="switchTab('categories')">📁 Categories</button> | |
| <button class="tab" onclick="switchTab('market')">💰 Market Data</button> | |
| <button class="tab" onclick="switchTab('health')">❤️ Health</button> | |
| </div> | |
| <!-- Overview Tab --> | |
| <div id="overview" class="tab-content active"> | |
| <div class="stats-grid"> | |
| <div class="stat-card blue"> | |
| <h3>Total Providers</h3> | |
| <div class="value" id="totalProviders">-</div> | |
| <div class="label">API Sources</div> | |
| </div> | |
| <div class="stat-card green"> | |
| <h3>Online</h3> | |
| <div class="value" id="onlineProviders">-</div> | |
| <div class="label">Working Perfectly</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> | |
| <div class="grid-2"> | |
| <div class="card"> | |
| <h2>🔌 Recent Provider Status</h2> | |
| <div id="recentProviders"> | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| Loading providers... | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>📈 System Health</h2> | |
| <div id="systemHealth"> | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| Loading health data... | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Providers Tab --> | |
| <div id="providers" class="tab-content"> | |
| <div class="card"> | |
| <h2>🔌 All Providers</h2> | |
| <input type="text" class="search-box" id="providerSearch" placeholder="Search providers..." onkeyup="filterProviders()"> | |
| <div class="providers-grid" id="allProviders"> | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| Loading providers... | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Categories Tab --> | |
| <div id="categories" class="tab-content"> | |
| <div class="card"> | |
| <h2>📁 Categories Breakdown</h2> | |
| <div class="category-list" id="categoriesList"> | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| Loading categories... | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Market Tab --> | |
| <div id="market" class="tab-content"> | |
| <div class="card"> | |
| <h2>💰 Market Data</h2> | |
| <div id="marketData"> | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| Loading market data... | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Health Tab --> | |
| <div id="health" class="tab-content"> | |
| <div class="stats-grid"> | |
| <div class="stat-card purple"> | |
| <h3>Uptime</h3> | |
| <div class="value" id="uptimePercent">-</div> | |
| <div class="label">Overall Health</div> | |
| </div> | |
| <div class="stat-card blue"> | |
| <h3>Avg Response</h3> | |
| <div class="value" id="avgResponse">-</div> | |
| <div class="label">Milliseconds</div> | |
| </div> | |
| <div class="stat-card green"> | |
| <h3>Categories</h3> | |
| <div class="value" id="totalCategories">-</div> | |
| <div class="label">Data Types</div> | |
| </div> | |
| <div class="stat-card orange"> | |
| <h3>Last Check</h3> | |
| <div class="value" id="lastCheck" style="font-size: 18px;">-</div> | |
| <div class="label">Timestamp</div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>📊 Detailed Health Report</h2> | |
| <div id="healthDetails"> | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| Loading health details... | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let allProvidersData = []; | |
| let currentTab = 'overview'; | |
| function switchTab(tabName) { | |
| // Hide all tabs | |
| document.querySelectorAll('.tab-content').forEach(tab => { | |
| tab.classList.remove('active'); | |
| }); | |
| document.querySelectorAll('.tab').forEach(tab => { | |
| tab.classList.remove('active'); | |
| }); | |
| // Show selected tab | |
| document.getElementById(tabName).classList.add('active'); | |
| event.target.classList.add('active'); | |
| currentTab = tabName; | |
| // Load data for specific tabs | |
| if (tabName === 'market') { | |
| loadMarketData(); | |
| } | |
| } | |
| async function loadProviders() { | |
| try { | |
| const response = await fetch('/api/providers'); | |
| const providers = await response.json(); | |
| allProvidersData = providers; | |
| // Calculate stats | |
| 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); | |
| // Update stats | |
| 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 + '%'; | |
| // Calculate average response time | |
| const responseTimes = providers.filter(p => p.response_time_ms).map(p => p.response_time_ms); | |
| const avgResp = responseTimes.length > 0 ? | |
| Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length) : 0; | |
| document.getElementById('avgResponse').textContent = avgResp + 'ms'; | |
| // Group by category | |
| 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; | |
| document.getElementById('lastCheck').textContent = new Date().toLocaleTimeString(); | |
| // Display recent providers (first 10) | |
| displayRecentProviders(providers.slice(0, 10)); | |
| // Display all providers | |
| displayAllProviders(providers); | |
| // Display categories | |
| displayCategories(categories); | |
| // Display health details | |
| displayHealthDetails(providers); | |
| } catch (error) { | |
| console.error('Error loading providers:', error); | |
| } | |
| } | |
| function displayRecentProviders(providers) { | |
| const html = 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}</span> | |
| ${p.response_time_ms ? `<div class="response-time">${Math.round(p.response_time_ms)}ms</div>` : ''} | |
| </div> | |
| `).join(''); | |
| document.getElementById('recentProviders').innerHTML = `<div class="providers-grid" style="max-height: 400px;">${html}</div>`; | |
| } | |
| function displayAllProviders(providers) { | |
| const html = providers.map(p => ` | |
| <div class="provider-card ${p.status}" data-name="${p.name.toLowerCase()}" data-category="${p.category.toLowerCase()}"> | |
| <div class="name">${p.name}</div> | |
| <div class="category">${p.category}</div> | |
| <span class="status ${p.status}">${p.status}</span> | |
| ${p.response_time_ms ? `<div class="response-time">⚡ ${Math.round(p.response_time_ms)}ms</div>` : ''} | |
| </div> | |
| `).join(''); | |
| document.getElementById('allProviders').innerHTML = html; | |
| } | |
| function displayCategories(categories) { | |
| const html = Object.entries(categories).map(([name, stats]) => ` | |
| <div class="category-item"> | |
| <div class="name">${name}</div> | |
| <div class="stats"> | |
| <div class="stat"> | |
| <span class="status-indicator online"></span> | |
| <span>${stats.online} online</span> | |
| </div> | |
| <div class="stat"> | |
| <span class="status-indicator degraded"></span> | |
| <span>${stats.degraded} degraded</span> | |
| </div> | |
| <div class="stat"> | |
| <span class="status-indicator offline"></span> | |
| <span>${stats.offline} offline</span> | |
| </div> | |
| <div class="stat" style="margin-left: auto; color: #3b82f6; font-weight: 600;"> | |
| ${stats.total} total | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| document.getElementById('categoriesList').innerHTML = html; | |
| } | |
| function displayHealthDetails(providers) { | |
| const online = providers.filter(p => p.status === 'online'); | |
| const degraded = providers.filter(p => p.status === 'degraded'); | |
| const offline = providers.filter(p => p.status === 'offline'); | |
| const html = ` | |
| <div style="display: grid; gap: 20px;"> | |
| <div class="category-item" style="border-left-color: #10b981;"> | |
| <div class="name">✅ Online Providers (${online.length})</div> | |
| <div style="color: #94a3b8; margin-top: 10px;"> | |
| ${online.slice(0, 5).map(p => p.name).join(', ')} | |
| ${online.length > 5 ? ` and ${online.length - 5} more...` : ''} | |
| </div> | |
| </div> | |
| <div class="category-item" style="border-left-color: #f59e0b;"> | |
| <div class="name">⚠️ Degraded Providers (${degraded.length})</div> | |
| <div style="color: #94a3b8; margin-top: 10px;"> | |
| ${degraded.length > 0 ? degraded.map(p => p.name).join(', ') : 'None'} | |
| </div> | |
| </div> | |
| <div class="category-item" style="border-left-color: #ef4444;"> | |
| <div class="name">❌ Offline Providers (${offline.length})</div> | |
| <div style="color: #94a3b8; margin-top: 10px;"> | |
| ${offline.length > 0 ? offline.map(p => p.name).join(', ') : 'None'} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| document.getElementById('healthDetails').innerHTML = html; | |
| const systemHealthHtml = ` | |
| <div style="display: grid; gap: 15px;"> | |
| <div style="padding: 15px; background: #0f172a; border-radius: 8px;"> | |
| <div style="color: #94a3b8; font-size: 14px; margin-bottom: 5px;">System Status</div> | |
| <div style="font-size: 24px; font-weight: 600; color: ${online.length >= providers.length * 0.7 ? '#10b981' : '#f59e0b'};"> | |
| ${online.length >= providers.length * 0.7 ? '✅ Healthy' : '⚠️ Degraded'} | |
| </div> | |
| </div> | |
| <div style="padding: 15px; background: #0f172a; border-radius: 8px;"> | |
| <div style="color: #94a3b8; font-size: 14px; margin-bottom: 5px;">Availability</div> | |
| <div style="font-size: 24px; font-weight: 600; color: #3b82f6;"> | |
| ${((online.length / providers.length) * 100).toFixed(1)}% | |
| </div> | |
| </div> | |
| <div style="padding: 15px; background: #0f172a; border-radius: 8px;"> | |
| <div style="color: #94a3b8; font-size: 14px; margin-bottom: 5px;">Total Checks</div> | |
| <div style="font-size: 24px; font-weight: 600; color: #8b5cf6;"> | |
| ${providers.length} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| document.getElementById('systemHealth').innerHTML = systemHealthHtml; | |
| } | |
| async function loadMarketData() { | |
| try { | |
| const response = await fetch('/api/market'); | |
| const data = await response.json(); | |
| if (data.cryptocurrencies && data.cryptocurrencies.length > 0) { | |
| const html = data.cryptocurrencies.map(coin => ` | |
| <div class="market-item"> | |
| <div class="coin-info"> | |
| <div> | |
| <div class="coin-name">${coin.name}</div> | |
| <div class="coin-symbol">${coin.symbol}</div> | |
| </div> | |
| </div> | |
| <div style="display: flex; align-items: center; gap: 20px;"> | |
| <div class="price">$${coin.price.toLocaleString()}</div> | |
| <div class="change ${coin.change_24h >= 0 ? 'positive' : 'negative'}"> | |
| ${coin.change_24h >= 0 ? '↑' : '↓'} ${Math.abs(coin.change_24h).toFixed(2)}% | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| document.getElementById('marketData').innerHTML = html; | |
| } else { | |
| document.getElementById('marketData').innerHTML = '<div class="empty-state"><h3>No market data available</h3><p>Market data providers may be offline</p></div>'; | |
| } | |
| } catch (error) { | |
| console.error('Error loading market data:', error); | |
| document.getElementById('marketData').innerHTML = '<div class="empty-state"><h3>Error loading market data</h3></div>'; | |
| } | |
| } | |
| function filterProviders() { | |
| const search = document.getElementById('providerSearch').value.toLowerCase(); | |
| const cards = document.querySelectorAll('#allProviders .provider-card'); | |
| cards.forEach(card => { | |
| const name = card.getAttribute('data-name'); | |
| const category = card.getAttribute('data-category'); | |
| if (name.includes(search) || category.includes(search)) { | |
| card.style.display = 'block'; | |
| } else { | |
| card.style.display = 'none'; | |
| } | |
| }); | |
| } | |
| function refreshData() { | |
| loadProviders(); | |
| if (currentTab === 'market') { | |
| loadMarketData(); | |
| } | |
| } | |
| function exportData() { | |
| const dataStr = JSON.stringify(allProvidersData, null, 2); | |
| const dataBlob = new Blob([dataStr], {type: 'application/json'}); | |
| const url = URL.createObjectURL(dataBlob); | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.download = `crypto-monitor-${new Date().toISOString()}.json`; | |
| link.click(); | |
| } | |
| // Load data on start | |
| loadProviders(); | |
| // Auto-refresh every 30 seconds | |
| setInterval(refreshData, 30000); | |
| </script> | |
| </body> | |
| </html> | |