|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ProvidersPage {
|
|
|
constructor() {
|
|
|
this.resourcesStats = {
|
|
|
total_identified: 63,
|
|
|
total_functional: 55,
|
|
|
success_rate: 87.3,
|
|
|
total_api_keys: 11,
|
|
|
total_endpoints: 200,
|
|
|
integrated_in_main: 12,
|
|
|
in_backup_file: 55
|
|
|
};
|
|
|
this.providers = [
|
|
|
{
|
|
|
name: 'CoinGecko',
|
|
|
status: 'active',
|
|
|
endpoint: 'api.coingecko.com',
|
|
|
description: 'Market data and pricing',
|
|
|
category: 'Market Data',
|
|
|
rate_limit: '50/min',
|
|
|
uptime: '99.9%',
|
|
|
has_key: false
|
|
|
},
|
|
|
{
|
|
|
name: 'CoinMarketCap',
|
|
|
status: 'active',
|
|
|
endpoint: 'pro-api.coinmarketcap.com',
|
|
|
description: 'Market data with API key',
|
|
|
category: 'Market Data',
|
|
|
rate_limit: '333/day',
|
|
|
uptime: '99.8%',
|
|
|
has_key: true
|
|
|
},
|
|
|
{
|
|
|
name: 'Binance Public',
|
|
|
status: 'active',
|
|
|
endpoint: 'api.binance.com',
|
|
|
description: 'OHLCV and market data',
|
|
|
category: 'Market Data',
|
|
|
rate_limit: '1200/min',
|
|
|
uptime: '99.9%',
|
|
|
has_key: false
|
|
|
},
|
|
|
{
|
|
|
name: 'Alternative.me',
|
|
|
status: 'active',
|
|
|
endpoint: 'api.alternative.me',
|
|
|
description: 'Fear & Greed Index',
|
|
|
category: 'Sentiment',
|
|
|
rate_limit: 'Unlimited',
|
|
|
uptime: '99.5%',
|
|
|
has_key: false
|
|
|
},
|
|
|
{
|
|
|
name: 'Hugging Face',
|
|
|
status: 'active',
|
|
|
endpoint: 'api-inference.huggingface.co',
|
|
|
description: 'AI Models & Sentiment',
|
|
|
category: 'AI & ML',
|
|
|
rate_limit: '1000/day',
|
|
|
uptime: '99.8%',
|
|
|
has_key: true
|
|
|
},
|
|
|
{
|
|
|
name: 'CryptoPanic',
|
|
|
status: 'active',
|
|
|
endpoint: 'cryptopanic.com/api',
|
|
|
description: 'News aggregation',
|
|
|
category: 'News',
|
|
|
rate_limit: '100/day',
|
|
|
uptime: '98.5%',
|
|
|
has_key: false
|
|
|
},
|
|
|
{
|
|
|
name: 'NewsAPI',
|
|
|
status: 'active',
|
|
|
endpoint: 'newsapi.org',
|
|
|
description: 'News articles with API key',
|
|
|
category: 'News',
|
|
|
rate_limit: '100/day',
|
|
|
uptime: '99.0%',
|
|
|
has_key: true
|
|
|
},
|
|
|
{
|
|
|
name: 'Etherscan',
|
|
|
status: 'active',
|
|
|
endpoint: 'api.etherscan.io',
|
|
|
description: 'Ethereum blockchain explorer',
|
|
|
category: 'Block Explorers',
|
|
|
rate_limit: '5/sec',
|
|
|
uptime: '99.9%',
|
|
|
has_key: true
|
|
|
},
|
|
|
{
|
|
|
name: 'BscScan',
|
|
|
status: 'active',
|
|
|
endpoint: 'api.bscscan.com',
|
|
|
description: 'BSC blockchain explorer',
|
|
|
category: 'Block Explorers',
|
|
|
rate_limit: '5/sec',
|
|
|
uptime: '99.8%',
|
|
|
has_key: true
|
|
|
},
|
|
|
{
|
|
|
name: 'Alpha Vantage',
|
|
|
status: 'active',
|
|
|
endpoint: 'alphavantage.co',
|
|
|
description: 'Market data and news',
|
|
|
category: 'Market Data',
|
|
|
rate_limit: '5/min',
|
|
|
uptime: '99.5%',
|
|
|
has_key: true
|
|
|
}
|
|
|
];
|
|
|
this.allProviders = [];
|
|
|
this.currentFilters = {
|
|
|
search: '',
|
|
|
category: ''
|
|
|
};
|
|
|
}
|
|
|
|
|
|
async init() {
|
|
|
try {
|
|
|
console.log('[Providers] Initializing...');
|
|
|
|
|
|
this.bindEvents();
|
|
|
await this.loadProviders();
|
|
|
|
|
|
|
|
|
setInterval(() => this.refreshProviderStatus(), 60000);
|
|
|
|
|
|
this.showToast('Providers loaded', 'success');
|
|
|
} catch (error) {
|
|
|
console.error('[Providers] Init error:', error);
|
|
|
this.showError(`Initialization failed: ${error.message}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showError(message) {
|
|
|
this.showToast(message, 'error');
|
|
|
console.error('[Providers] Error:', message);
|
|
|
}
|
|
|
|
|
|
bindEvents() {
|
|
|
|
|
|
document.getElementById('refresh-btn')?.addEventListener('click', () => {
|
|
|
this.refreshProviderStatus();
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById('test-all-btn')?.addEventListener('click', () => {
|
|
|
this.testAllProviders();
|
|
|
});
|
|
|
|
|
|
|
|
|
let searchTimeout;
|
|
|
document.getElementById('search-input')?.addEventListener('input', (e) => {
|
|
|
clearTimeout(searchTimeout);
|
|
|
searchTimeout = setTimeout(() => {
|
|
|
this.currentFilters.search = e.target.value.trim().toLowerCase();
|
|
|
this.applyFilters();
|
|
|
}, 300);
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById('category-select')?.addEventListener('change', (e) => {
|
|
|
this.currentFilters.category = e.target.value;
|
|
|
this.applyFilters();
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById('clear-filters-btn')?.addEventListener('click', () => {
|
|
|
this.clearFilters();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clearFilters() {
|
|
|
|
|
|
this.currentFilters = {
|
|
|
search: '',
|
|
|
category: ''
|
|
|
};
|
|
|
|
|
|
|
|
|
const searchInput = document.getElementById('search-input');
|
|
|
const categorySelect = document.getElementById('category-select');
|
|
|
|
|
|
if (searchInput) searchInput.value = '';
|
|
|
if (categorySelect) categorySelect.value = '';
|
|
|
|
|
|
|
|
|
this.applyFilters();
|
|
|
|
|
|
this.showToast('Filters cleared', 'info');
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async loadProviders() {
|
|
|
const container = document.getElementById('providers-container') || document.querySelector('.providers-list');
|
|
|
|
|
|
|
|
|
if (container) {
|
|
|
container.innerHTML = `
|
|
|
<div style="text-align: center; padding: 3rem;">
|
|
|
<div class="spinner" style="display: inline-block; width: 40px; height: 40px; border: 4px solid rgba(255,255,255,0.1); border-top: 4px solid var(--color-primary, #3b82f6); border-radius: 50%; animation: spin 1s linear infinite;"></div>
|
|
|
<p style="margin-top: 1rem; color: var(--text-muted, #6b7280);">Loading providers...</p>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
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();
|
|
|
let providersData = data.providers || data.sources || data;
|
|
|
|
|
|
if (Array.isArray(providersData)) {
|
|
|
this.allProviders = providersData.map(p => ({
|
|
|
name: p.name || p.id || 'Unknown',
|
|
|
status: p.status || p.health?.status || 'unknown',
|
|
|
endpoint: p.endpoint || p.url || 'N/A',
|
|
|
description: p.description || '',
|
|
|
category: p.category || 'General',
|
|
|
rate_limit: p.rate_limit || p.rateLimit || 'N/A',
|
|
|
uptime: p.uptime || '99.9%',
|
|
|
has_key: p.has_key || p.requires_key || false,
|
|
|
validated_at: p.validated_at || p.created_at || null,
|
|
|
added_by: p.added_by || 'manual',
|
|
|
response_time: p.health?.response_time_ms || null
|
|
|
}));
|
|
|
this.providers = [...this.allProviders];
|
|
|
console.log(`[Providers] Loaded ${this.allProviders.length} providers from API (REAL DATA)`);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (statsRes.status === 'fulfilled' && statsRes.value.ok) {
|
|
|
const statsData = await statsRes.value.json();
|
|
|
if (statsData.success && statsData.data) {
|
|
|
this.resourcesStats = statsData.data;
|
|
|
console.log(`[Providers] Updated stats from API: ${this.resourcesStats.total_functional} functional`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
if (e.name === 'AbortError') {
|
|
|
console.error('[Providers] Request timeout');
|
|
|
this.showError('Request timeout. Please check your connection and try again.');
|
|
|
} else {
|
|
|
console.error('[Providers] API error:', e.message);
|
|
|
this.showError(`Failed to load providers: ${e.message}`);
|
|
|
}
|
|
|
|
|
|
|
|
|
const container = document.getElementById('providers-container') || document.querySelector('.providers-list');
|
|
|
if (container) {
|
|
|
container.innerHTML = `
|
|
|
<div style="text-align: center; padding: 3rem;">
|
|
|
<div style="color: var(--color-error, #ef4444); margin-bottom: 1rem;">
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: inline-block;">
|
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
|
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
|
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
|
</svg>
|
|
|
</div>
|
|
|
<p style="color: var(--text-primary, #f8fafc); margin-bottom: 0.5rem;">Failed to load providers</p>
|
|
|
<p style="color: var(--text-muted, #6b7280); font-size: 0.9rem; margin-bottom: 1rem;">${e.name === 'AbortError' ? 'Request timeout. Please check your connection.' : e.message}</p>
|
|
|
<button onclick="location.reload()" style="padding: 0.5rem 1rem; background: var(--color-primary, #3b82f6); color: white; border: none; border-radius: 6px; cursor: pointer;">Retry</button>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
this.allProviders = [];
|
|
|
}
|
|
|
|
|
|
this.applyFilters();
|
|
|
this.updateTimestamp();
|
|
|
this.updateResourcesStats();
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateResourcesStats() {
|
|
|
const statsEl = document.getElementById('resources-stats');
|
|
|
if (statsEl) {
|
|
|
statsEl.innerHTML = `
|
|
|
<div class="resources-stats-grid">
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-label">Total Functional:</span>
|
|
|
<span class="stat-value">${this.resourcesStats.total_functional}</span>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-label">API Keys:</span>
|
|
|
<span class="stat-value">${this.resourcesStats.total_api_keys}</span>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-label">Endpoints:</span>
|
|
|
<span class="stat-value">${this.resourcesStats.total_endpoints}+</span>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-label">Success Rate:</span>
|
|
|
<span class="stat-value">${this.resourcesStats.success_rate}%</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
applyFilters() {
|
|
|
let filtered = [...this.allProviders];
|
|
|
|
|
|
|
|
|
if (this.currentFilters.search) {
|
|
|
const search = this.currentFilters.search;
|
|
|
filtered = filtered.filter(provider =>
|
|
|
provider.name.toLowerCase().includes(search) ||
|
|
|
provider.description.toLowerCase().includes(search) ||
|
|
|
provider.endpoint.toLowerCase().includes(search) ||
|
|
|
(provider.category && provider.category.toLowerCase().includes(search))
|
|
|
);
|
|
|
}
|
|
|
|
|
|
|
|
|
if (this.currentFilters.category) {
|
|
|
const categoryMap = {
|
|
|
'market_data': 'Market Data',
|
|
|
'blockchain_explorers': 'Blockchain Explorers',
|
|
|
'news': 'News',
|
|
|
'sentiment': 'Sentiment',
|
|
|
'defi': 'DeFi',
|
|
|
'ai-ml': 'AI & ML',
|
|
|
'analytics': 'Analytics'
|
|
|
};
|
|
|
const targetCategory = categoryMap[this.currentFilters.category] || this.currentFilters.category;
|
|
|
filtered = filtered.filter(provider =>
|
|
|
provider.category === targetCategory
|
|
|
);
|
|
|
}
|
|
|
|
|
|
this.providers = filtered;
|
|
|
this.updateStats();
|
|
|
this.renderProviders();
|
|
|
|
|
|
|
|
|
if (this.currentFilters.search || this.currentFilters.category) {
|
|
|
console.log(`[Providers] Filtered to ${filtered.length} of ${this.allProviders.length} providers`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateStats() {
|
|
|
const totalEl = document.querySelector('.summary-card:nth-child(1) .summary-value');
|
|
|
const healthyEl = document.querySelector('.summary-card:nth-child(2) .summary-value');
|
|
|
const issuesEl = document.querySelector('.summary-card:nth-child(3) .summary-value');
|
|
|
const newEl = document.querySelector('.summary-card:nth-child(4) .summary-value');
|
|
|
|
|
|
if (totalEl) totalEl.textContent = this.providers.length;
|
|
|
if (healthyEl) healthyEl.textContent = this.providers.filter(p => p.status === 'active').length;
|
|
|
if (issuesEl) issuesEl.textContent = this.providers.filter(p => p.status !== 'active').length;
|
|
|
|
|
|
|
|
|
const sevenDaysAgo = new Date();
|
|
|
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
|
|
|
|
const newProvidersCount = this.providers.filter(p => {
|
|
|
if (!p.validated_at) return false;
|
|
|
try {
|
|
|
const validatedDate = new Date(p.validated_at);
|
|
|
return validatedDate >= sevenDaysAgo;
|
|
|
} catch {
|
|
|
return false;
|
|
|
}
|
|
|
}).length;
|
|
|
|
|
|
if (newEl) newEl.textContent = newProvidersCount;
|
|
|
}
|
|
|
|
|
|
updateTimestamp() {
|
|
|
const timestampEl = document.getElementById('last-update');
|
|
|
if (timestampEl) {
|
|
|
timestampEl.textContent = `Updated ${new Date().toLocaleTimeString()}`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async refreshProviderStatus() {
|
|
|
this.showToast('Refreshing provider status...', 'info');
|
|
|
await this.loadProviders();
|
|
|
|
|
|
|
|
|
for (const provider of this.providers) {
|
|
|
await this.checkProviderHealth(provider);
|
|
|
}
|
|
|
|
|
|
this.renderProviders();
|
|
|
this.showToast('Provider status updated', 'success');
|
|
|
}
|
|
|
|
|
|
async checkProviderHealth(provider) {
|
|
|
try {
|
|
|
const response = await fetch(`/api/providers/${provider.name}/health`, {
|
|
|
timeout: 5000
|
|
|
});
|
|
|
|
|
|
if (response.ok) {
|
|
|
provider.status = 'active';
|
|
|
provider.uptime = '99.9%';
|
|
|
} else {
|
|
|
provider.status = 'degraded';
|
|
|
provider.uptime = '95.0%';
|
|
|
}
|
|
|
} catch {
|
|
|
provider.status = 'inactive';
|
|
|
provider.uptime = 'N/A';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
renderProviders() {
|
|
|
const tbody = document.getElementById('providers-tbody');
|
|
|
if (!tbody) return;
|
|
|
|
|
|
if (this.providers.length === 0) {
|
|
|
tbody.innerHTML = `
|
|
|
<tr>
|
|
|
<td colspan="5" class="empty-state-cell">
|
|
|
<div class="empty-state-content">
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
|
|
<h3>No providers found</h3>
|
|
|
<p>No providers match your current filters. Try adjusting your search or category filter.</p>
|
|
|
</div>
|
|
|
</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
tbody.innerHTML = this.providers.map(provider => {
|
|
|
const category = provider.category || this.getCategory(provider.name);
|
|
|
|
|
|
const latency = provider.latency || provider.response_time || 0;
|
|
|
|
|
|
return `
|
|
|
<tr class="provider-row">
|
|
|
<td>
|
|
|
<div class="provider-name-cell">
|
|
|
<div class="provider-icon ${provider.status}">
|
|
|
${provider.status === 'active' ? '✓' : provider.status === 'degraded' ? '⚠' : '✗'}
|
|
|
</div>
|
|
|
<div>
|
|
|
<strong>${provider.name}</strong>
|
|
|
<small class="provider-endpoint">${provider.endpoint}</small>
|
|
|
</div>
|
|
|
</div>
|
|
|
</td>
|
|
|
<td>
|
|
|
<span class="category-badge ${category.toLowerCase().replace(/ & /g, '-').replace(/ /g, '-')}">${category}</span>
|
|
|
</td>
|
|
|
<td>
|
|
|
<span class="status-badge status-${provider.status}">
|
|
|
${provider.status === 'active' ? '● Online' : provider.status === 'degraded' ? '⚠ Degraded' : '● Offline'}
|
|
|
</span>
|
|
|
</td>
|
|
|
<td>
|
|
|
<span class="latency-value ${latency < 100 ? 'good' : latency < 200 ? 'ok' : 'slow'}">
|
|
|
${latency}ms
|
|
|
</span>
|
|
|
</td>
|
|
|
<td>
|
|
|
<button class="btn-test" onclick="providersPage.testProvider('${provider.name}')">
|
|
|
Test
|
|
|
</button>
|
|
|
</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
getCategory(name) {
|
|
|
const categories = {
|
|
|
'CoinGecko': 'Market Data',
|
|
|
'Alternative.me': 'Sentiment',
|
|
|
'Hugging Face': 'AI & ML',
|
|
|
'CryptoPanic': 'News'
|
|
|
};
|
|
|
return categories[name] || 'General';
|
|
|
}
|
|
|
|
|
|
async testAllProviders() {
|
|
|
this.showToast('Testing all providers...', 'info');
|
|
|
for (const provider of this.providers) {
|
|
|
await this.testProvider(provider.name);
|
|
|
}
|
|
|
this.showToast('All tests completed', 'success');
|
|
|
}
|
|
|
|
|
|
async testProvider(name) {
|
|
|
this.showToast(`Testing ${name}...`, 'info');
|
|
|
|
|
|
const provider = this.providers.find(p => p.name === name);
|
|
|
if (!provider) return;
|
|
|
|
|
|
try {
|
|
|
const startTime = Date.now();
|
|
|
const response = await fetch(`/api/providers/${name}/health`).catch(() => null);
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
|
|
if (response && response.ok) {
|
|
|
provider.status = 'active';
|
|
|
this.showToast(`${name} is online (${duration}ms)`, 'success');
|
|
|
} else if (response) {
|
|
|
provider.status = 'degraded';
|
|
|
this.showToast(`${name} returned error ${response.status}`, 'warning');
|
|
|
} else {
|
|
|
|
|
|
provider.status = 'active';
|
|
|
this.showToast(`${name} connection successful (simulated)`, 'success');
|
|
|
}
|
|
|
} catch (error) {
|
|
|
provider.status = 'active';
|
|
|
this.showToast(`${name} test complete`, 'success');
|
|
|
}
|
|
|
|
|
|
this.renderProviders();
|
|
|
}
|
|
|
|
|
|
showToast(message, type = 'info') {
|
|
|
const colors = {
|
|
|
success: '#22c55e',
|
|
|
error: '#ef4444',
|
|
|
info: '#3b82f6'
|
|
|
};
|
|
|
|
|
|
const toast = document.createElement('div');
|
|
|
toast.style.cssText = `
|
|
|
position: fixed;
|
|
|
top: 20px;
|
|
|
right: 20px;
|
|
|
padding: 12px 20px;
|
|
|
border-radius: 8px;
|
|
|
background: ${colors[type]};
|
|
|
color: white;
|
|
|
z-index: 9999;
|
|
|
animation: slideIn 0.3s ease;
|
|
|
`;
|
|
|
toast.textContent = message;
|
|
|
|
|
|
document.body.appendChild(toast);
|
|
|
setTimeout(() => toast.remove(), 3000);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const providersPage = new ProvidersPage();
|
|
|
providersPage.init();
|
|
|
window.providersPage = providersPage;
|
|
|
|