const DEFAULT_TTL = 60 * 1000; // 1 minute cache class ApiClient { constructor() { // Use current origin by default to avoid hardcoded URLs this.baseURL = window.location.origin; // Allow override via window.BACKEND_URL if needed if (typeof window.BACKEND_URL === 'string' && window.BACKEND_URL.trim()) { this.baseURL = window.BACKEND_URL.trim().replace(/\/$/, ''); } console.log('[ApiClient] Using Backend:', this.baseURL); this.cache = new Map(); this.requestLogs = []; this.errorLogs = []; this.logSubscribers = new Set(); this.errorSubscribers = new Set(); } buildUrl(endpoint) { if (!endpoint.startsWith('/')) { return `${this.baseURL}/${endpoint}`; } return `${this.baseURL}${endpoint}`; } notifyLog(entry) { this.requestLogs.push(entry); this.requestLogs = this.requestLogs.slice(-100); this.logSubscribers.forEach((cb) => cb(entry)); } notifyError(entry) { this.errorLogs.push(entry); this.errorLogs = this.errorLogs.slice(-100); this.errorSubscribers.forEach((cb) => cb(entry)); } onLog(callback) { this.logSubscribers.add(callback); return () => this.logSubscribers.delete(callback); } onError(callback) { this.errorSubscribers.add(callback); return () => this.errorSubscribers.delete(callback); } getLogs() { return [...this.requestLogs]; } getErrors() { return [...this.errorLogs]; } async request(method, endpoint, { body, cache = true, ttl = DEFAULT_TTL } = {}) { const url = this.buildUrl(endpoint); const cacheKey = `${method}:${url}`; if (method === 'GET' && cache && this.cache.has(cacheKey)) { const cached = this.cache.get(cacheKey); if (Date.now() - cached.timestamp < ttl) { return { ok: true, data: cached.data, cached: true }; } } const started = performance.now(); const randomId = (window.crypto && window.crypto.randomUUID && window.crypto.randomUUID()) || `${Date.now()}-${Math.random()}`; const entry = { id: randomId, method, endpoint, status: 'pending', duration: 0, time: new Date().toISOString(), }; try { const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined, }); const duration = performance.now() - started; entry.duration = Math.round(duration); entry.status = response.status; const contentType = response.headers.get('content-type') || ''; let data = null; if (contentType.includes('application/json')) { data = await response.json(); } else if (contentType.includes('text')) { data = await response.text(); } if (!response.ok) { const error = new Error((data && data.message) || response.statusText || 'Unknown error'); error.status = response.status; throw error; } if (method === 'GET' && cache) { this.cache.set(cacheKey, { timestamp: Date.now(), data }); } this.notifyLog({ ...entry, success: true }); return { ok: true, data }; } catch (error) { const duration = performance.now() - started; entry.duration = Math.round(duration); entry.status = error.status || 'error'; this.notifyLog({ ...entry, success: false, error: error.message }); this.notifyError({ message: error.message, endpoint, method, time: new Date().toISOString(), }); return { ok: false, error: error.message }; } } get(endpoint, options) { return this.request('GET', endpoint, options); } post(endpoint, body, options = {}) { return this.request('POST', endpoint, { ...options, body }); } // ===== Specific API helpers ===== // Note: Backend uses api_server_extended.py which has different endpoints getHealth() { // Backend doesn't have /api/health, use /api/status instead return this.get('/api/status'); } getTopCoins(limit = 10) { // Backend uses /api/market which returns cryptocurrencies array return this.get('/api/market').then(result => { if (result.ok && result.data && result.data.cryptocurrencies) { return { ok: true, data: result.data.cryptocurrencies.slice(0, limit) }; } return result; }); } getCoinDetails(symbol) { // Get from market data and filter by symbol return this.get('/api/market').then(result => { if (result.ok && result.data && result.data.cryptocurrencies) { const coin = result.data.cryptocurrencies.find( c => c.symbol.toUpperCase() === symbol.toUpperCase() ); return coin ? { ok: true, data: coin } : { ok: false, error: 'Coin not found' }; } return result; }); } getMarketStats() { // Backend returns stats in /api/market response return this.get('/api/market').then(result => { if (result.ok && result.data) { return { ok: true, data: { total_market_cap: result.data.total_market_cap, btc_dominance: result.data.btc_dominance, total_volume_24h: result.data.total_volume_24h, market_cap_change_24h: result.data.market_cap_change_24h } }; } return result; }); } getLatestNews(limit = 20) { // Backend doesn't have news endpoint yet, return empty for now return Promise.resolve({ ok: true, data: { articles: [], message: 'News endpoint not yet implemented in backend' } }); } getProviders() { return this.get('/api/providers'); } getPriceChart(symbol, timeframe = '7d') { // Backend uses /api/ohlcv const cleanSymbol = encodeURIComponent(String(symbol || 'BTC').trim().toUpperCase()); // Map timeframe to interval and limit const intervalMap = { '1d': '1h', '7d': '1h', '30d': '4h', '90d': '1d', '365d': '1d' }; const limitMap = { '1d': 24, '7d': 168, '30d': 180, '90d': 90, '365d': 365 }; const interval = intervalMap[timeframe] || '1h'; const limit = limitMap[timeframe] || 168; return this.get(`/api/ohlcv?symbol=${cleanSymbol}USDT&interval=${interval}&limit=${limit}`); } analyzeChart(symbol, timeframe = '7d', indicators = []) { // Not implemented in backend yet return Promise.resolve({ ok: false, error: 'Chart analysis not yet implemented in backend' }); } runQuery(payload) { // Not implemented in backend yet return Promise.resolve({ ok: false, error: 'Query endpoint not yet implemented in backend' }); } analyzeSentiment(payload) { // Backend has /api/sentiment but it returns market sentiment, not text analysis // For now, return the market sentiment return this.get('/api/sentiment'); } summarizeNews(item) { // Not implemented in backend yet return Promise.resolve({ ok: false, error: 'News summarization not yet implemented in backend' }); } getDatasetsList() { // Not implemented in backend yet return Promise.resolve({ ok: true, data: { datasets: [], message: 'Datasets endpoint not yet implemented in backend' } }); } getDatasetSample(name) { // Not implemented in backend yet return Promise.resolve({ ok: false, error: 'Dataset sample not yet implemented in backend' }); } getModelsList() { // Backend has /api/hf/models return this.get('/api/hf/models'); } testModel(payload) { // Not implemented in backend yet return Promise.resolve({ ok: false, error: 'Model testing not yet implemented in backend' }); } // ===== Additional methods for backend compatibility ===== getTrending() { return this.get('/api/trending'); } getStats() { return this.get('/api/stats'); } getHFHealth() { return this.get('/api/hf/health'); } runDiagnostics(autoFix = false) { return this.post('/api/diagnostics/run', { auto_fix: autoFix }); } getLastDiagnostics() { return this.get('/api/diagnostics/last'); } runAPLScan() { return this.post('/api/apl/run'); } getAPLReport() { return this.get('/api/apl/report'); } getAPLSummary() { return this.get('/api/apl/summary'); } } const apiClient = new ApiClient(); export default apiClient;