| |
| |
| |
|
|
| class DataSourcesPage { |
| constructor() { |
| this.sources = []; |
| this.refreshInterval = null; |
| this.resourcesStats = { |
| total_identified: 63, |
| total_functional: 55, |
| success_rate: 87.3, |
| total_api_keys: 11, |
| total_endpoints: 200, |
| categories: { |
| market_data: { total: 13, with_key: 3, without_key: 10 }, |
| news: { total: 10, with_key: 2, without_key: 8 }, |
| sentiment: { total: 6, with_key: 0, without_key: 6 }, |
| analytics: { total: 13, with_key: 0, without_key: 13 }, |
| block_explorers: { total: 6, with_key: 5, without_key: 1 }, |
| rpc_nodes: { total: 8, with_key: 2, without_key: 6 }, |
| ai_ml: { total: 1, with_key: 1, without_key: 0 } |
| } |
| }; |
| } |
|
|
| async init() { |
| try { |
| console.log('[DataSources] Initializing...'); |
| this.bindEvents(); |
| await this.loadDataSources(); |
| |
| this.refreshInterval = setInterval(() => this.loadDataSources(), 60000); |
| |
| console.log('[DataSources] Ready'); |
| } catch (error) { |
| console.error('[DataSources] Init error:', error); |
| } |
| } |
|
|
| bindEvents() { |
| |
| const refreshBtn = document.getElementById('refresh-btn'); |
| if (refreshBtn) { |
| refreshBtn.addEventListener('click', async () => { |
| refreshBtn.classList.add('loading'); |
| refreshBtn.innerHTML = ` |
| <svg class="spinner-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"></path></svg> |
| Refreshing... |
| `; |
| await this.loadDataSources(); |
| refreshBtn.classList.remove('loading'); |
| refreshBtn.innerHTML = ` |
| <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg> |
| Refresh |
| `; |
| }); |
| } |
|
|
| |
| const testAllBtn = document.getElementById('test-all-btn'); |
| if (testAllBtn) { |
| testAllBtn.addEventListener('click', () => this.testAllSources()); |
| } |
|
|
| |
| const tabs = document.querySelectorAll('.tab'); |
| tabs.forEach(tab => { |
| tab.addEventListener('click', (e) => { |
| |
| tabs.forEach(t => t.classList.remove('active')); |
| |
| e.target.classList.add('active'); |
| |
| const category = e.target.dataset.category; |
| this.filterSources(category); |
| }); |
| }); |
|
|
| |
| const statCards = document.querySelectorAll('.stat-card'); |
| statCards.forEach(card => { |
| const label = card.querySelector('.stat-label')?.textContent.toLowerCase(); |
| if (!label) return; |
|
|
| card.style.cursor = 'pointer'; |
| |
| card.addEventListener('click', () => { |
| |
| statCards.forEach(c => c.classList.remove('active')); |
| card.classList.add('active'); |
|
|
| if (label.includes('active')) { |
| this.filterSourcesByStatus('active'); |
| } else if (label.includes('ohlcv')) { |
| |
| const ohlcvTab = document.querySelector('.tab[data-category="ohlcv"]'); |
| if (ohlcvTab) ohlcvTab.click(); |
| } else if (label.includes('free')) { |
| |
| this.filterSources('all'); |
| } else if (label.includes('total')) { |
| this.filterSources('all'); |
| } |
| }); |
| }); |
| } |
|
|
| filterSourcesByStatus(status) { |
| const filtered = this.sources.filter(source => source.status === status); |
| this.renderSources(filtered); |
| |
| |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); |
| } |
|
|
| filterSources(category) { |
| if (!category || category === 'all') { |
| this.renderSources(this.sources); |
| return; |
| } |
|
|
| const filtered = this.sources.filter(source => { |
| |
| const sourceCategory = (source.category || source.type || '').toLowerCase(); |
| return sourceCategory.includes(category.toLowerCase()); |
| }); |
| |
| this.renderSources(filtered); |
| } |
|
|
| async loadDataSources() { |
| try { |
| |
| const [providersRes, statsRes] = await Promise.allSettled([ |
| fetch('/api/providers', { signal: AbortSignal.timeout(10000) }), |
| fetch('/api/resources/stats', { signal: AbortSignal.timeout(10000) }) |
| ]); |
| |
| |
| if (providersRes.status === 'fulfilled' && providersRes.value.ok) { |
| const contentType = providersRes.value.headers.get('content-type'); |
| if (contentType && contentType.includes('application/json')) { |
| const data = await providersRes.value.json(); |
| this.sources = data.providers || data || []; |
| console.log(`[DataSources] Loaded ${this.sources.length} sources from API (REAL DATA)`); |
| } |
| } |
| |
| |
| if (statsRes.status === 'fulfilled' && statsRes.value.ok) { |
| const statsData = await statsRes.value.json(); |
| if (statsData.success && statsData.data) { |
| |
| this.resourcesStats = { |
| ...this.resourcesStats, |
| ...statsData.data |
| }; |
| console.log(`[DataSources] Updated stats from API: ${this.resourcesStats.total_functional} functional, ${this.resourcesStats.total_endpoints} endpoints`); |
| } |
| } else { |
| console.warn('[DataSources] Using fallback stats - API unavailable'); |
| } |
| |
| } catch (error) { |
| if (error.name === 'AbortError') { |
| console.error('[DataSources] Request timeout'); |
| } else { |
| console.error('[DataSources] API error:', error.message); |
| } |
| |
| this.sources = []; |
| } |
| |
| |
| this.updateStats(); |
| this.renderSources(this.sources); |
| } |
|
|
| updateStats() { |
| const totalEl = document.getElementById('total-endpoints'); |
| const activeEl = document.getElementById('active-sources'); |
| const keysEl = document.getElementById('api-keys'); |
| const successEl = document.getElementById('success-rate'); |
| |
| |
| if (totalEl) { |
| const totalCount = this.resourcesStats.total_endpoints || this.sources.length || 7; |
| totalEl.textContent = totalCount; |
| } |
| |
| if (activeEl) { |
| const activeCount = this.resourcesStats.total_functional || |
| this.sources.filter(s => s.status === 'active').length || |
| this.sources.length; |
| activeEl.textContent = activeCount; |
| } |
| |
| if (keysEl) { |
| const keysCount = this.resourcesStats.total_api_keys || |
| this.sources.filter(s => s.has_key || s.needs_auth).length || |
| 11; |
| keysEl.textContent = keysCount; |
| } |
| |
| if (successEl) { |
| const successRate = this.resourcesStats.success_rate || 87.3; |
| successEl.textContent = `${successRate.toFixed(1)}%`; |
| } |
| } |
| |
| updateResourcesStats() { |
| |
| |
| console.log('[DataSources] Stats updated from real API data'); |
| } |
| |
| getFallbackSources() { |
| return [ |
| { id: 'binance', name: 'Binance Public', category: 'Market Data', status: 'active', endpoint: 'api.binance.com/api/v3', has_key: false }, |
| { id: 'coingecko', name: 'CoinGecko', category: 'Market Data', status: 'active', endpoint: 'api.coingecko.com/api/v3', has_key: false }, |
| { id: 'coinmarketcap', name: 'CoinMarketCap', category: 'Market Data', status: 'active', endpoint: 'pro-api.coinmarketcap.com', has_key: true }, |
| { id: 'alternative', name: 'Alternative.me', category: 'Sentiment', status: 'active', endpoint: 'api.alternative.me/fng', has_key: false }, |
| { id: 'newsapi', name: 'NewsAPI', category: 'News', status: 'active', endpoint: 'newsapi.org/v2', has_key: true }, |
| { id: 'cryptopanic', name: 'CryptoPanic', category: 'News', status: 'active', endpoint: 'cryptopanic.com/api/v1', has_key: false }, |
| { id: 'etherscan', name: 'Etherscan', category: 'Block Explorers', status: 'active', endpoint: 'api.etherscan.io/api', has_key: true }, |
| { id: 'bscscan', name: 'BscScan', category: 'Block Explorers', status: 'active', endpoint: 'api.bscscan.com/api', has_key: true } |
| ]; |
| } |
|
|
| renderSources(sourcesToRender = this.sources) { |
| const container = document.getElementById('sources-container'); |
| if (!container) return; |
|
|
| if (!sourcesToRender || sourcesToRender.length === 0) { |
| container.innerHTML = ` |
| <div class="empty-state"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <circle cx="11" cy="11" r="8"></circle> |
| <line x1="21" y1="21" x2="16.65" y2="16.65"></line> |
| </svg> |
| <h3>No Data Sources</h3> |
| <p>No data sources found for this category. Try refreshing or check API connection.</p> |
| </div> |
| `; |
| return; |
| } |
|
|
| container.innerHTML = sourcesToRender.map(source => { |
| const health = source.health || source.health_status || 'unknown'; |
| const responseTime = source.response_time || source.health?.response_time_ms || null; |
| const hasKey = source.has_key || source.needs_auth || false; |
| |
| return ` |
| <div class="source-card"> |
| <div class="source-header"> |
| <div class="source-title-group"> |
| <h3>${source.name || source.id || 'Unknown'}</h3> |
| ${hasKey ? '<span class="key-badge" title="Requires API Key">🔑</span>' : ''} |
| </div> |
| <span class="status-badge status-${health}">${health}</span> |
| </div> |
| <div class="source-body"> |
| <div class="source-detail"> |
| <span class="label">Category:</span> |
| <span class="value">${source.category || 'N/A'}</span> |
| </div> |
| <div class="source-detail"> |
| <span class="label">Endpoint:</span> |
| <span class="value code">${source.endpoint || source.url || 'N/A'}</span> |
| </div> |
| ${responseTime ? ` |
| <div class="source-detail"> |
| <span class="label">Response Time:</span> |
| <span class="value ${responseTime < 200 ? 'good' : responseTime < 500 ? 'ok' : 'slow'}">${responseTime}ms</span> |
| </div> |
| ` : ''} |
| ${source.rate_limit ? ` |
| <div class="source-detail"> |
| <span class="label">Rate Limit:</span> |
| <span class="value">${source.rate_limit}</span> |
| </div> |
| ` : ''} |
| </div> |
| <div class="source-actions"> |
| <button class="btn-sm btn-test" onclick="window.dataSourcesPage.testSource('${source.id || source.name}')"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline> |
| </svg> |
| Test |
| </button> |
| </div> |
| </div> |
| `; |
| }).join(''); |
| } |
|
|
| async testSource(sourceId) { |
| console.log('[DataSources] Testing source:', sourceId); |
| try { |
| const response = await fetch(`/api/providers/${sourceId}/health`); |
| const data = await response.json(); |
| alert(`Source ${sourceId}: ${data.status || 'unknown'}`); |
| await this.loadDataSources(); |
| } catch (error) { |
| alert(`Failed to test source: ${error.message}`); |
| } |
| } |
|
|
| async testAllSources() { |
| console.log('[DataSources] Testing all sources...'); |
| for (const source of this.sources) { |
| await this.testSource(source.id); |
| } |
| } |
| } |
|
|
| export default DataSourcesPage; |
|
|