|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ProviderDiscoveryEngine {
|
|
|
constructor() {
|
|
|
this.providers = [];
|
|
|
this.categories = new Map();
|
|
|
this.healthStatus = new Map();
|
|
|
this.configPath = '/static/providers_config_ultimate.json';
|
|
|
this.initialized = false;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async init() {
|
|
|
if (this.initialized) return;
|
|
|
|
|
|
console.log('[Provider Discovery] Initializing...');
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.loadProvidersFromAPI();
|
|
|
} catch (error) {
|
|
|
console.warn('[Provider Discovery] API load failed, trying JSON file:', error);
|
|
|
|
|
|
await this.loadProvidersFromJSON();
|
|
|
}
|
|
|
|
|
|
this.categorizeProviders();
|
|
|
this.startHealthMonitoring();
|
|
|
|
|
|
this.initialized = true;
|
|
|
console.log(`[Provider Discovery] Initialized with ${this.providers.length} providers in ${this.categories.size} categories`);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async loadProvidersFromAPI() {
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/providers/config');
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
this.processProviderData(data);
|
|
|
} catch (error) {
|
|
|
throw new Error(`Failed to load from API: ${error.message}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async loadProvidersFromJSON() {
|
|
|
try {
|
|
|
const response = await fetch(this.configPath);
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
this.processProviderData(data);
|
|
|
} catch (error) {
|
|
|
console.error('[Provider Discovery] Failed to load JSON:', error);
|
|
|
|
|
|
this.useFallbackConfig();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processProviderData(data) {
|
|
|
if (!data || !data.providers) {
|
|
|
throw new Error('Invalid provider data structure');
|
|
|
}
|
|
|
|
|
|
|
|
|
this.providers = Object.entries(data.providers).map(([id, provider]) => ({
|
|
|
id,
|
|
|
...provider,
|
|
|
status: 'unknown',
|
|
|
lastCheck: null,
|
|
|
responseTime: null
|
|
|
}));
|
|
|
|
|
|
console.log(`[Provider Discovery] Loaded ${this.providers.length} providers`);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
categorizeProviders() {
|
|
|
this.categories.clear();
|
|
|
|
|
|
this.providers.forEach(provider => {
|
|
|
const category = provider.category || 'other';
|
|
|
|
|
|
if (!this.categories.has(category)) {
|
|
|
this.categories.set(category, []);
|
|
|
}
|
|
|
|
|
|
this.categories.get(category).push(provider);
|
|
|
});
|
|
|
|
|
|
|
|
|
this.categories.forEach((providers, category) => {
|
|
|
providers.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
|
});
|
|
|
|
|
|
console.log(`[Provider Discovery] Categorized into: ${Array.from(this.categories.keys()).join(', ')}`);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getAllProviders() {
|
|
|
return this.providers;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getProvidersByCategory(category) {
|
|
|
return this.categories.get(category) || [];
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getCategories() {
|
|
|
return Array.from(this.categories.keys());
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
searchProviders(query) {
|
|
|
const lowerQuery = query.toLowerCase();
|
|
|
return this.providers.filter(provider =>
|
|
|
provider.name.toLowerCase().includes(lowerQuery) ||
|
|
|
provider.id.toLowerCase().includes(lowerQuery) ||
|
|
|
(provider.category || '').toLowerCase().includes(lowerQuery)
|
|
|
);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
filterProviders(filters = {}) {
|
|
|
let filtered = [...this.providers];
|
|
|
|
|
|
if (filters.category) {
|
|
|
filtered = filtered.filter(p => p.category === filters.category);
|
|
|
}
|
|
|
|
|
|
if (filters.free !== undefined) {
|
|
|
filtered = filtered.filter(p => p.free === filters.free);
|
|
|
}
|
|
|
|
|
|
if (filters.requiresAuth !== undefined) {
|
|
|
filtered = filtered.filter(p => p.requires_auth === filters.requiresAuth);
|
|
|
}
|
|
|
|
|
|
if (filters.status) {
|
|
|
filtered = filtered.filter(p => p.status === filters.status);
|
|
|
}
|
|
|
|
|
|
return filtered;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getStats() {
|
|
|
const total = this.providers.length;
|
|
|
const free = this.providers.filter(p => p.free).length;
|
|
|
const paid = total - free;
|
|
|
const requiresAuth = this.providers.filter(p => p.requires_auth).length;
|
|
|
|
|
|
const statuses = {
|
|
|
online: this.providers.filter(p => p.status === 'online').length,
|
|
|
offline: this.providers.filter(p => p.status === 'offline').length,
|
|
|
unknown: this.providers.filter(p => p.status === 'unknown').length
|
|
|
};
|
|
|
|
|
|
return {
|
|
|
total,
|
|
|
free,
|
|
|
paid,
|
|
|
requiresAuth,
|
|
|
categories: this.categories.size,
|
|
|
statuses
|
|
|
};
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async checkProviderHealth(providerId) {
|
|
|
const provider = this.providers.find(p => p.id === providerId);
|
|
|
if (!provider) return null;
|
|
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/providers/${providerId}/health`, {
|
|
|
timeout: 5000
|
|
|
});
|
|
|
|
|
|
const responseTime = Date.now() - startTime;
|
|
|
const status = response.ok ? 'online' : 'offline';
|
|
|
|
|
|
|
|
|
provider.status = status;
|
|
|
provider.lastCheck = new Date();
|
|
|
provider.responseTime = responseTime;
|
|
|
|
|
|
this.healthStatus.set(providerId, {
|
|
|
status,
|
|
|
lastCheck: provider.lastCheck,
|
|
|
responseTime
|
|
|
});
|
|
|
|
|
|
return { status, responseTime };
|
|
|
} catch (error) {
|
|
|
provider.status = 'offline';
|
|
|
provider.lastCheck = new Date();
|
|
|
provider.responseTime = null;
|
|
|
|
|
|
this.healthStatus.set(providerId, {
|
|
|
status: 'offline',
|
|
|
lastCheck: provider.lastCheck,
|
|
|
error: error.message
|
|
|
});
|
|
|
|
|
|
return { status: 'offline', error: error.message };
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
startHealthMonitoring(interval = 60000) {
|
|
|
|
|
|
setInterval(async () => {
|
|
|
const highPriorityProviders = this.providers
|
|
|
.filter(p => (p.priority || 0) >= 8)
|
|
|
.slice(0, 5);
|
|
|
|
|
|
for (const provider of highPriorityProviders) {
|
|
|
await this.checkProviderHealth(provider.id);
|
|
|
}
|
|
|
|
|
|
console.log('[Provider Discovery] Health check completed');
|
|
|
}, interval);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generateProviderCard(provider) {
|
|
|
const statusColors = {
|
|
|
online: 'var(--color-accent-green)',
|
|
|
offline: 'var(--color-accent-red)',
|
|
|
unknown: 'var(--color-text-secondary)'
|
|
|
};
|
|
|
|
|
|
const statusColor = statusColors[provider.status] || statusColors.unknown;
|
|
|
const icon = this.getCategoryIcon(provider.category);
|
|
|
|
|
|
return `
|
|
|
<div class="provider-card glass-effect" data-provider-id="${provider.id}">
|
|
|
<div class="provider-card-header">
|
|
|
<div class="provider-icon">
|
|
|
${window.getIcon ? window.getIcon(icon, 32) : ''}
|
|
|
</div>
|
|
|
<div class="provider-info">
|
|
|
<h3 class="provider-name">${provider.name}</h3>
|
|
|
<span class="provider-category">${this.formatCategory(provider.category)}</span>
|
|
|
</div>
|
|
|
<div class="provider-status" style="color: ${statusColor}">
|
|
|
<span class="status-dot" style="background: ${statusColor}"></span>
|
|
|
${provider.status}
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="provider-card-body">
|
|
|
<div class="provider-meta">
|
|
|
<div class="meta-item">
|
|
|
<span class="meta-label">Type:</span>
|
|
|
<span class="meta-value">${provider.free ? 'Free' : 'Paid'}</span>
|
|
|
</div>
|
|
|
<div class="meta-item">
|
|
|
<span class="meta-label">Auth:</span>
|
|
|
<span class="meta-value">${provider.requires_auth ? 'Required' : 'No'}</span>
|
|
|
</div>
|
|
|
<div class="meta-item">
|
|
|
<span class="meta-label">Priority:</span>
|
|
|
<span class="meta-value">${provider.priority || 'N/A'}/10</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
${this.generateRateLimitInfo(provider)}
|
|
|
|
|
|
<div class="provider-actions">
|
|
|
<button class="btn-secondary btn-sm" onclick="providerDiscovery.checkProviderHealth('${provider.id}')">
|
|
|
${window.getIcon ? window.getIcon('refresh', 16) : ''} Test
|
|
|
</button>
|
|
|
${provider.docs_url ? `
|
|
|
<a href="${provider.docs_url}" target="_blank" class="btn-secondary btn-sm">
|
|
|
${window.getIcon ? window.getIcon('fileText', 16) : ''} Docs
|
|
|
</a>
|
|
|
` : ''}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
generateRateLimitInfo(provider) {
|
|
|
if (!provider.rate_limit) return '';
|
|
|
|
|
|
const limits = [];
|
|
|
if (provider.rate_limit.requests_per_second) {
|
|
|
limits.push(`${provider.rate_limit.requests_per_second}/sec`);
|
|
|
}
|
|
|
if (provider.rate_limit.requests_per_minute) {
|
|
|
limits.push(`${provider.rate_limit.requests_per_minute}/min`);
|
|
|
}
|
|
|
if (provider.rate_limit.requests_per_hour) {
|
|
|
limits.push(`${provider.rate_limit.requests_per_hour}/hr`);
|
|
|
}
|
|
|
if (provider.rate_limit.requests_per_day) {
|
|
|
limits.push(`${provider.rate_limit.requests_per_day}/day`);
|
|
|
}
|
|
|
|
|
|
if (limits.length === 0) return '';
|
|
|
|
|
|
return `
|
|
|
<div class="provider-rate-limit">
|
|
|
<span class="rate-limit-label">Rate Limit:</span>
|
|
|
<span class="rate-limit-value">${limits.join(', ')}</span>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getCategoryIcon(category) {
|
|
|
const icons = {
|
|
|
market_data: 'barChart',
|
|
|
exchange: 'activity',
|
|
|
blockchain_explorer: 'database',
|
|
|
defi: 'layers',
|
|
|
sentiment: 'activity',
|
|
|
news: 'newspaper',
|
|
|
social: 'users',
|
|
|
rpc: 'server',
|
|
|
analytics: 'pieChart',
|
|
|
whale_tracking: 'trendingUp',
|
|
|
ml_model: 'brain'
|
|
|
};
|
|
|
|
|
|
return icons[category] || 'globe';
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
formatCategory(category) {
|
|
|
if (!category) return 'Other';
|
|
|
return category.split('_').map(word =>
|
|
|
word.charAt(0).toUpperCase() + word.slice(1)
|
|
|
).join(' ');
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
renderProviders(containerId, options = {}) {
|
|
|
const container = document.getElementById(containerId);
|
|
|
if (!container) {
|
|
|
console.error(`Container "${containerId}" not found`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
let providers = this.providers;
|
|
|
|
|
|
|
|
|
if (options.category) {
|
|
|
providers = this.getProvidersByCategory(options.category);
|
|
|
}
|
|
|
if (options.search) {
|
|
|
providers = this.searchProviders(options.search);
|
|
|
}
|
|
|
if (options.filters) {
|
|
|
providers = this.filterProviders(options.filters);
|
|
|
}
|
|
|
|
|
|
|
|
|
if (options.sortBy) {
|
|
|
providers = [...providers].sort((a, b) => {
|
|
|
if (options.sortBy === 'name') {
|
|
|
return a.name.localeCompare(b.name);
|
|
|
}
|
|
|
if (options.sortBy === 'priority') {
|
|
|
return (b.priority || 0) - (a.priority || 0);
|
|
|
}
|
|
|
return 0;
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
if (options.limit) {
|
|
|
providers = providers.slice(0, options.limit);
|
|
|
}
|
|
|
|
|
|
|
|
|
const html = providers.map(p => this.generateProviderCard(p)).join('');
|
|
|
container.innerHTML = html;
|
|
|
|
|
|
console.log(`[Provider Discovery] Rendered ${providers.length} providers`);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
renderCategoryTabs(containerId) {
|
|
|
const container = document.getElementById(containerId);
|
|
|
if (!container) return;
|
|
|
|
|
|
const categories = this.getCategories();
|
|
|
const html = categories.map(category => {
|
|
|
const count = this.getProvidersByCategory(category).length;
|
|
|
return `
|
|
|
<button class="category-tab" data-category="${category}">
|
|
|
${window.getIcon ? window.getIcon(this.getCategoryIcon(category), 20) : ''}
|
|
|
<span>${this.formatCategory(category)}</span>
|
|
|
<span class="category-count">${count}</span>
|
|
|
</button>
|
|
|
`;
|
|
|
}).join('');
|
|
|
|
|
|
container.innerHTML = html;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useFallbackConfig() {
|
|
|
console.warn('[Provider Discovery] Using minimal fallback config');
|
|
|
this.providers = [
|
|
|
{
|
|
|
id: 'coingecko',
|
|
|
name: 'CoinGecko',
|
|
|
category: 'market_data',
|
|
|
free: true,
|
|
|
requires_auth: false,
|
|
|
priority: 10,
|
|
|
status: 'unknown'
|
|
|
},
|
|
|
{
|
|
|
id: 'binance',
|
|
|
name: 'Binance',
|
|
|
category: 'exchange',
|
|
|
free: true,
|
|
|
requires_auth: false,
|
|
|
priority: 10,
|
|
|
status: 'unknown'
|
|
|
}
|
|
|
];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
window.providerDiscovery = new ProviderDiscoveryEngine();
|
|
|
|
|
|
console.log('[Provider Discovery] Engine loaded');
|
|
|
|