/** * Crypto API Hub Page */ import { formatNumber } from '../../shared/js/utils/formatters.js'; import logger from '../../shared/js/utils/logger.js'; class CryptoAPIHubPage { constructor() { this.currentFilter = 'all'; this.apis = []; } /** * Escape HTML to prevent XSS * @param {string} text - Text to escape * @returns {string} Escaped text */ escapeHtml(text) { if (typeof text !== 'string') { return String(text); } const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } async init() { try { logger.info('CryptoAPIHub', 'Initializing...'); this.bindEvents(); await this.loadAPIs(); logger.info('CryptoAPIHub', 'Ready'); } catch (error) { logger.error('CryptoAPIHub', 'Init error:', error); } } /** * Bind event listeners to UI elements */ bindEvents() { logger.debug('CryptoAPIHub', 'Binding events...'); // Search functionality const searchInput = document.getElementById('api-search'); if (searchInput) { searchInput.addEventListener('input', (e) => { this.filterAPIs(e.target.value); }); logger.debug('CryptoAPIHub', 'Search input bound'); } else { logger.warn('CryptoAPIHub', 'Search input #api-search not found'); } // Filter buttons const filterButtons = document.querySelectorAll('.filter-btn'); if (filterButtons.length > 0) { filterButtons.forEach(btn => { btn.addEventListener('click', (e) => { filterButtons.forEach(b => b.classList.remove('active')); e.target.classList.add('active'); this.currentFilter = e.target.dataset.filter; logger.debug('CryptoAPIHub', `Filter changed to: ${this.currentFilter}`); this.renderAPIs(); }); }); logger.debug('CryptoAPIHub', `Bound ${filterButtons.length} filter buttons`); } else { logger.warn('CryptoAPIHub', 'No filter buttons (.filter-btn) found'); } // API Tester Button const testerBtn = document.getElementById('api-tester-btn'); if (testerBtn) { testerBtn.addEventListener('click', () => { logger.debug('CryptoAPIHub', 'Opening API tester modal'); this.openTesterModal(); }); logger.debug('CryptoAPIHub', 'API tester button bound'); } else { logger.warn('CryptoAPIHub', 'API tester button #api-tester-btn not found'); } // Export Button const exportBtn = document.getElementById('export-btn'); if (exportBtn) { exportBtn.addEventListener('click', () => { logger.debug('CryptoAPIHub', 'Exporting APIs'); this.exportAPIs(); }); logger.debug('CryptoAPIHub', 'Export button bound'); } else { logger.warn('CryptoAPIHub', 'Export button #export-btn not found'); } // Modal Close Buttons const closeBtn = document.getElementById('modal-close-btn'); if (closeBtn) { closeBtn.addEventListener('click', () => this.closeTesterModal()); logger.debug('CryptoAPIHub', 'Modal close button bound'); } const modalOverlay = document.querySelector('.modal-overlay'); if (modalOverlay) { modalOverlay.addEventListener('click', (e) => { // Only close if clicking the overlay itself, not its children if (e.target === modalOverlay) { this.closeTesterModal(); } }); logger.debug('CryptoAPIHub', 'Modal overlay bound'); } // Escape key to close modal document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { const modal = document.getElementById('api-tester-modal'); if (modal && modal.classList.contains('active')) { this.closeTesterModal(); } } }); // Modal Tester Logic const sendRequestBtn = document.getElementById('send-request-btn'); if (sendRequestBtn) { sendRequestBtn.addEventListener('click', () => this.sendTestRequest()); logger.debug('CryptoAPIHub', 'Send request button bound'); } // HTTP Method buttons const methodButtons = document.querySelectorAll('.method-btn'); if (methodButtons.length > 0) { methodButtons.forEach(btn => { btn.addEventListener('click', (e) => { methodButtons.forEach(b => b.classList.remove('active')); e.target.classList.add('active'); // Show/hide body input based on method const method = e.target.dataset.method; const bodyGroup = document.getElementById('body-group'); if (bodyGroup) { bodyGroup.style.display = (method === 'POST' || method === 'PUT') ? 'block' : 'none'; } }); }); logger.debug('CryptoAPIHub', `Bound ${methodButtons.length} method buttons`); } logger.debug('CryptoAPIHub', 'Event binding complete'); } openTesterModal(apiId = null) { const modal = document.getElementById('api-tester-modal'); if (modal) { modal.classList.add('active'); if (apiId) { const api = this.apis.find(a => a.id === apiId); if (api) { const urlInput = document.getElementById('test-url'); if (urlInput) urlInput.value = api.base_url || api.url || ''; } } } } /** * Close the API tester modal */ closeTesterModal() { const modal = document.getElementById('api-tester-modal'); if (modal) { modal.classList.remove('active'); logger.debug('CryptoAPIHub', 'Modal closed'); } } exportAPIs() { if (!Array.isArray(this.apis) || this.apis.length === 0) { alert('No APIs to export'); return; } const dataStr = JSON.stringify(this.apis, null, 2); const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr); const exportFileDefaultName = 'crypto-apis-export.json'; const linkElement = document.createElement('a'); linkElement.setAttribute('href', dataUri); linkElement.setAttribute('download', exportFileDefaultName); linkElement.click(); } async sendTestRequest() { const url = document.getElementById('test-url')?.value; const method = document.querySelector('.method-btn.active')?.dataset.method || 'GET'; const headersStr = document.getElementById('test-headers')?.value; const bodyStr = document.getElementById('test-body')?.value; const responseContainer = document.getElementById('response-container'); const responseJson = document.getElementById('response-json'); if (!url) { alert('Please enter a URL'); return; } if (responseContainer) responseContainer.style.display = 'block'; if (responseJson) responseJson.textContent = 'Loading...'; try { let headers = {}; if (headersStr) { try { headers = JSON.parse(headersStr); } catch (e) { alert('Invalid JSON in headers'); return; } } let body = undefined; if ((method === 'POST' || method === 'PUT') && bodyStr) { try { body = JSON.parse(bodyStr); } catch (e) { alert('Invalid JSON in body'); return; } } // Use the proxy endpoint if needed, or direct fetch if CORS allows. // Using direct fetch for now as user instructions imply client-side testing, // but usually we need a backend proxy to avoid CORS. // There is a /api/crypto-hub/test endpoint in the other JS file, // but here we might use a simple fetch first. // Note: For the fix, we'll use direct fetch but catch errors. const options = { method, headers: { 'Content-Type': 'application/json', ...headers } }; if (body) { options.body = JSON.stringify(body); } const res = await fetch(url, options); const data = await res.json().catch(() => ({ status: res.status, statusText: res.statusText })); if (responseJson) { responseJson.textContent = JSON.stringify(data, null, 2); } } catch (error) { if (responseJson) { responseJson.textContent = 'Error: ' + error.message; } } } /** * Load APIs from backend with retry logic * @param {number} retryCount - Current retry attempt (internal use) * @param {number} maxRetries - Maximum number of retries * @returns {Promise} */ async loadAPIs(retryCount = 0, maxRetries = 2) { const container = document.getElementById('apis-container'); let errorMessage = 'Failed to load APIs'; // Show loading state if (container && retryCount === 0) { container.innerHTML = `

Loading APIs...

`; } try { logger.debug('CryptoAPIHub', `Loading APIs from /api/resources/apis... (attempt ${retryCount + 1}/${maxRetries + 1})`); // Use dynamic base URL for Hugging Face deployment const baseUrl = window.location.origin; const apiUrl = `${baseUrl}/api/resources/apis`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout let response; try { response = await fetch(apiUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, signal: controller.signal }); } catch (fetchError) { clearTimeout(timeoutId); if (fetchError.name === 'AbortError') { throw new Error('Request timeout: Server took too long to respond'); } throw fetchError; } finally { clearTimeout(timeoutId); } // Log response details for debugging logger.debug('CryptoAPIHub', 'Response status:', response.status, response.statusText); logger.debug('CryptoAPIHub', 'Response headers:', Object.fromEntries(response.headers.entries())); // Check if response is OK if (!response.ok) { // Try to extract error message from JSON response let errorData = null; const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) { try { const responseText = await response.text(); if (responseText && responseText.trim().length > 0) { errorData = JSON.parse(responseText); errorMessage = errorData.message || errorData.error || `HTTP ${response.status}: ${response.statusText}`; } else { errorMessage = `HTTP ${response.status}: ${response.statusText}`; } } catch (parseError) { logger.warn('CryptoAPIHub', 'Failed to parse error response as JSON:', parseError); errorMessage = `HTTP ${response.status}: ${response.statusText}`; } } else { // Try to get text error try { const errorText = await response.text(); errorMessage = errorText || `HTTP ${response.status}: ${response.statusText}`; } catch (textError) { errorMessage = `HTTP ${response.status}: ${response.statusText}`; } } // Log full error details for debugging logger.error('CryptoAPIHub', 'API request failed:', { status: response.status, statusText: response.statusText, errorMessage: errorMessage, errorData: errorData, url: apiUrl, timestamp: new Date().toISOString() }); // Retry on 500 errors if we haven't exceeded max retries if (response.status === 500 && retryCount < maxRetries) { const delay = Math.min(1000 * Math.pow(2, retryCount), 5000); // Exponential backoff, max 5s logger.info('CryptoAPIHub', `Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); if (container) { container.innerHTML = `

Server error. Retrying...

`; } await new Promise(resolve => setTimeout(resolve, delay)); return this.loadAPIs(retryCount + 1, maxRetries); } throw new Error(errorMessage); } // Validate content type const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) { logger.warn('CryptoAPIHub', 'Unexpected content type:', contentType); // Still try to parse as JSON if possible } // Parse JSON response let data; try { const responseText = await response.text(); if (!responseText || responseText.trim().length === 0) { throw new Error('Empty response from server'); } data = JSON.parse(responseText); } catch (parseError) { logger.error('CryptoAPIHub', 'JSON parse error:', parseError); throw new Error(`Invalid JSON response: ${parseError.message}`); } // Validate data structure if (!data || typeof data !== 'object') { throw new Error('Invalid response: expected object, got ' + typeof data); } // Check for error flag in response if (data.error === true || data.ok === false) { errorMessage = data.message || 'API returned an error'; throw new Error(errorMessage); } logger.debug('CryptoAPIHub', 'Received data:', data); // Handle various data structures from different endpoints let apiList = []; if (Array.isArray(data)) { apiList = data; } else if (Array.isArray(data.apis)) { // Standard format with all APIs: { apis: [...] } apiList = data.apis; logger.debug('CryptoAPIHub', `Loaded ${apiList.length} APIs from data.apis`); } else if (data.local_routes && Array.isArray(data.local_routes.routes)) { // Legacy format - local routes only apiList = data.local_routes.routes.map(route => ({ id: route.path || route.name, name: route.name || route.path, category: route.category || 'local', description: route.description || route.summary || '', endpoints: route.endpoints_count || 1, endpoints_count: route.endpoints_count || 1, requires_key: route.requires_auth || false, free: !route.requires_auth, url: route.path || '', base_url: route.path || '' })); } else if (data.providers && Array.isArray(data.providers)) { // Providers format apiList = data.providers; } else { logger.warn('CryptoAPIHub', 'Unexpected data format, trying to extract:', data); // Try to find any array in the response for (const key in data) { if (Array.isArray(data[key]) && data[key].length > 0) { logger.debug('CryptoAPIHub', `Found array at key: ${key}`); apiList = data[key]; break; } } } // Validate apiList is an array if (!Array.isArray(apiList)) { logger.warn('CryptoAPIHub', 'apiList is not an array, defaulting to empty:', typeof apiList); apiList = []; } // Normalize the API list to ensure consistent structure this.apis = apiList.map(api => { // Validate each API item if (!api || typeof api !== 'object') { logger.warn('CryptoAPIHub', 'Invalid API item, skipping:', api); return null; } return { id: String(api.id || api.name || api.path || ''), name: String(api.name || api.title || api.path || 'Unknown'), category: String(api.category || 'general'), description: String(api.description || api.summary || ''), endpoints: Number(api.endpoints || api.endpoints_count || 0) || 0, endpoints_count: Number(api.endpoints_count || api.endpoints || 0) || 0, requires_key: Boolean(api.requires_key || api.requires_auth || false), free: api.free !== undefined ? Boolean(api.free) : !Boolean(api.requires_key || api.requires_auth), url: String(api.url || api.base_url || api.path || ''), base_url: String(api.base_url || api.url || api.path || ''), status: String(api.status || 'unknown') }; }).filter(api => api !== null); // Remove null entries logger.info('CryptoAPIHub', `Loaded ${this.apis.length} APIs`); this.renderAPIs(); this.updateStats(); } catch (error) { // Log full error details for debugging const errorDetails = { message: error.message, name: error.name, stack: error.stack, endpoint: '/api/resources/apis', retryCount: retryCount, maxRetries: maxRetries, timestamp: new Date().toISOString() }; logger.error('CryptoAPIHub', 'Load error:', error); console.error('[CryptoAPIHub] Failed to load APIs:', errorDetails); // Determine user-friendly error message if (error.name === 'AbortError' || error.message.includes('timeout')) { errorMessage = 'Request timed out. The server took too long to respond. Please check your connection and try again.'; } else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError') || error.message.includes('network')) { errorMessage = 'Network error. Please check your internet connection and try again.'; } else if (error.message.includes('500') || error.message.includes('Internal Server Error')) { errorMessage = 'Server error. The server encountered an internal error. Please try again in a moment.'; } else if (error.message.includes('404')) { errorMessage = 'API endpoint not found. Please contact support if this problem persists.'; } else { errorMessage = error.message || 'Unknown error occurred while loading APIs.'; } // Retry on network errors if we haven't exceeded max retries if ((error.name === 'AbortError' || error.message.includes('timeout') || error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) && retryCount < maxRetries) { const delay = Math.min(1000 * Math.pow(2, retryCount), 5000); // Exponential backoff, max 5s logger.info('CryptoAPIHub', `Retrying after network error in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); if (container) { container.innerHTML = `

Connection issue. Retrying...

`; } await new Promise(resolve => setTimeout(resolve, delay)); return this.loadAPIs(retryCount + 1, maxRetries); } // Show user-friendly error message with retry option if (container) { container.innerHTML = `

⚠️ Failed to load APIs

${this.escapeHtml(errorMessage)}

If this problem persists, please check the browser console for details.

`; } // Reset state to prevent undefined errors this.apis = []; this.renderAPIs(); this.updateStats(); } } renderAPIs() { const container = document.getElementById('apis-container'); if (!container) { logger.warn('CryptoAPIHub', 'Container #apis-container not found'); return; } // Ensure this.apis is an array if (!Array.isArray(this.apis)) { logger.warn('CryptoAPIHub', 'this.apis is not an array, resetting to empty array'); this.apis = []; } let filtered = this.apis; if (this.currentFilter !== 'all') { // Additional safety check if (typeof this.apis.filter === 'function') { filtered = this.apis.filter(api => api.category === this.currentFilter); } else { filtered = []; } } if (filtered.length === 0) { container.innerHTML = '
No APIs found
'; return; } container.innerHTML = filtered.map(api => `

${api.name || api.title || 'Unknown API'}

${api.category || 'General'}

${api.description || 'No description available'}

Endpoints: ${api.endpoints_count || api.endpoints || 0} ${(api.requires_key || !api.free) ? '🔑 Requires Key' : '✅ Free'}
`).join(''); } filterAPIs(query) { const cards = document.querySelectorAll('.api-card'); const lowerQuery = query.toLowerCase(); cards.forEach(card => { const text = card.textContent.toLowerCase(); card.style.display = text.includes(lowerQuery) ? 'block' : 'none'; }); } /** * Update statistics display */ updateStats() { if (!Array.isArray(this.apis)) { logger.warn('CryptoAPIHub', 'this.apis is not an array in updateStats'); this.apis = []; } const totalAPIs = this.apis.length; const freeAPIs = this.apis.filter(api => api.free || !api.requires_key).length; const categories = [...new Set(this.apis.map(api => api.category).filter(Boolean))].length; const totalEndpoints = this.apis.reduce((sum, api) => sum + (api.endpoints_count || api.endpoints || 0), 0); // Update total services const totalEl = document.getElementById('total-services'); if (totalEl) totalEl.textContent = totalAPIs; // Update total endpoints const endpointsEl = document.getElementById('total-endpoints'); if (endpointsEl) endpointsEl.textContent = totalEndpoints > 0 ? totalEndpoints : '150+'; // Update categories (if element exists) const catEl = document.getElementById('categories-count'); if (catEl) catEl.textContent = categories; logger.debug('CryptoAPIHub', `Stats updated: ${totalAPIs} APIs, ${freeAPIs} free, ${categories} categories`); } /** * View API details * @param {string} apiId - API identifier */ viewAPI(apiId) { const api = this.apis.find(a => a.id === apiId); if (api) { const details = ` API: ${api.name} Category: ${api.category} Endpoints: ${api.endpoints_count || api.endpoints || 0} ${api.url ? 'URL: ' + api.url : ''} Status: ${api.status || 'Unknown'} Auth Required: ${api.requires_key ? 'Yes' : 'No'} Description: ${api.description || 'N/A'} `.trim(); alert(details); } else { logger.warn('CryptoAPIHub', `API not found: ${apiId}`); } } /** * Test API using the modal * @param {string} apiId - API identifier */ testAPI(apiId) { // Use the internal modal instead of navigating away this.openTesterModal(apiId); } } export default CryptoAPIHubPage;