|
|
const DEFAULT_TTL = 60 * 1000; |
|
|
|
|
|
class ApiClient { |
|
|
constructor() { |
|
|
|
|
|
this.baseURL = window.location.origin; |
|
|
|
|
|
|
|
|
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 }); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getHealth() { |
|
|
|
|
|
return this.get('/api/status'); |
|
|
} |
|
|
|
|
|
getTopCoins(limit = 10) { |
|
|
|
|
|
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) { |
|
|
|
|
|
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() { |
|
|
|
|
|
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) { |
|
|
|
|
|
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') { |
|
|
|
|
|
const cleanSymbol = encodeURIComponent(String(symbol || 'BTC').trim().toUpperCase()); |
|
|
|
|
|
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 = []) { |
|
|
|
|
|
return Promise.resolve({ |
|
|
ok: false, |
|
|
error: 'Chart analysis not yet implemented in backend' |
|
|
}); |
|
|
} |
|
|
|
|
|
runQuery(payload) { |
|
|
|
|
|
return Promise.resolve({ |
|
|
ok: false, |
|
|
error: 'Query endpoint not yet implemented in backend' |
|
|
}); |
|
|
} |
|
|
|
|
|
analyzeSentiment(payload) { |
|
|
|
|
|
|
|
|
return this.get('/api/sentiment'); |
|
|
} |
|
|
|
|
|
summarizeNews(item) { |
|
|
|
|
|
return Promise.resolve({ |
|
|
ok: false, |
|
|
error: 'News summarization not yet implemented in backend' |
|
|
}); |
|
|
} |
|
|
|
|
|
getDatasetsList() { |
|
|
|
|
|
return Promise.resolve({ |
|
|
ok: true, |
|
|
data: { |
|
|
datasets: [], |
|
|
message: 'Datasets endpoint not yet implemented in backend' |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
getDatasetSample(name) { |
|
|
|
|
|
return Promise.resolve({ |
|
|
ok: false, |
|
|
error: 'Dataset sample not yet implemented in backend' |
|
|
}); |
|
|
} |
|
|
|
|
|
getModelsList() { |
|
|
|
|
|
return this.get('/api/hf/models'); |
|
|
} |
|
|
|
|
|
testModel(payload) { |
|
|
|
|
|
return Promise.resolve({ |
|
|
ok: false, |
|
|
error: 'Model testing not yet implemented in backend' |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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; |