/** * ============================================ * PROVIDER AUTO-DISCOVERY ENGINE * Enterprise Edition - Crypto Monitor Ultimate * ============================================ * * Automatically discovers and manages 200+ API providers * Features: * - Auto-loads providers from JSON config * - Categorizes providers (market, exchange, defi, news, etc.) * - Health checking & status monitoring * - Dynamic UI injection * - Search & filtering * - Rate limit tracking */ class ProviderDiscoveryEngine { constructor() { this.providers = []; this.categories = new Map(); this.healthStatus = new Map(); this.configPath = '/static/providers_config_ultimate.json'; // Fallback path this.initialized = false; } /** * Initialize the discovery engine */ async init() { if (this.initialized) return; console.log('[Provider Discovery] Initializing...'); try { // Try to load from backend API first await this.loadProvidersFromAPI(); } catch (error) { console.warn('[Provider Discovery] API load failed, trying JSON file:', error); // Fallback to JSON file 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`); } /** * Load providers from backend API */ async loadProvidersFromAPI() { try { // Try the new /api/providers/config endpoint first 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}`); } } /** * Load providers from JSON file */ 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); // Use fallback minimal config this.useFallbackConfig(); } } /** * Process provider data from any source */ processProviderData(data) { if (!data || !data.providers) { throw new Error('Invalid provider data structure'); } // Convert object to array 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`); } /** * Categorize 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); }); // Sort providers within each category by priority 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(', ')}`); } /** * Get all providers */ getAllProviders() { return this.providers; } /** * Get providers by category */ getProvidersByCategory(category) { return this.categories.get(category) || []; } /** * Get all categories */ getCategories() { return Array.from(this.categories.keys()); } /** * Search providers */ 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) ); } /** * Filter providers */ 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; } /** * Get provider statistics */ 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 }; } /** * Health check for a single provider */ async checkProviderHealth(providerId) { const provider = this.providers.find(p => p.id === providerId); if (!provider) return null; const startTime = Date.now(); try { // Call backend health check endpoint const response = await fetch(`/api/providers/${providerId}/health`, { timeout: 5000 }); const responseTime = Date.now() - startTime; const status = response.ok ? 'online' : 'offline'; // Update provider status 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 }; } } /** * Start health monitoring (periodic checks) */ startHealthMonitoring(interval = 60000) { // Check a few high-priority providers periodically 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); } /** * Generate provider card HTML */ 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 `