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