|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { CONFIG, API_BASE_URL, API_ENDPOINTS, CACHE_TTL, buildApiUrl, getCacheKey } from './config.js'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class APIClient { |
|
|
constructor(baseURL = API_BASE_URL) { |
|
|
this.baseURL = baseURL; |
|
|
this.cache = new Map(); |
|
|
this.cacheTTL = CACHE_TTL.market || 30000; |
|
|
this.maxRetries = CONFIG.MAX_RETRIES || 3; |
|
|
this.retryDelay = CONFIG.RETRY_DELAY || 1000; |
|
|
this.requestLog = []; |
|
|
this.errorLog = []; |
|
|
this.maxLogSize = 100; |
|
|
this.pendingRequests = new Map(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async request(endpoint, options = {}) { |
|
|
const url = `${this.baseURL}${endpoint}`; |
|
|
const method = options.method || 'GET'; |
|
|
const cacheKey = this._getCacheKey(url, options.params); |
|
|
const startTime = performance.now(); |
|
|
|
|
|
|
|
|
if (method === 'GET' && !options.skipCache) { |
|
|
|
|
|
const shouldSkipCache = endpoint.includes('/models/status') || |
|
|
endpoint.includes('/models/summary') || |
|
|
options.forceRefresh; |
|
|
|
|
|
if (!shouldSkipCache) { |
|
|
const cached = this._getFromCache(cacheKey, options.ttl); |
|
|
if (cached) { |
|
|
console.log(`[APIClient] Cache hit: ${endpoint}`); |
|
|
return cached; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (this.pendingRequests.has(cacheKey)) { |
|
|
console.log(`[APIClient] Deduplicating request: ${endpoint}`); |
|
|
return this.pendingRequests.get(cacheKey); |
|
|
} |
|
|
|
|
|
|
|
|
const urlWithParams = this._buildURL(url, options.params); |
|
|
|
|
|
|
|
|
let lastError; |
|
|
const requestPromise = (async () => { |
|
|
for (let attempt = 1; attempt <= this.maxRetries; attempt++) { |
|
|
try { |
|
|
const response = await fetch(urlWithParams, { |
|
|
method, |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
...options.headers, |
|
|
}, |
|
|
body: options.body ? JSON.stringify(options.body) : undefined, |
|
|
signal: options.signal, |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
|
|
} |
|
|
|
|
|
const data = await response.json(); |
|
|
const duration = performance.now() - startTime; |
|
|
|
|
|
|
|
|
if (method === 'GET' && !endpoint.includes('/models/status') && !endpoint.includes('/models/summary')) { |
|
|
this._saveToCache(cacheKey, data, options.ttl); |
|
|
} |
|
|
|
|
|
|
|
|
this._logRequest({ |
|
|
method, |
|
|
endpoint, |
|
|
status: response.status, |
|
|
duration: Math.round(duration), |
|
|
timestamp: Date.now(), |
|
|
}); |
|
|
|
|
|
this.pendingRequests.delete(cacheKey); |
|
|
return data; |
|
|
|
|
|
} catch (error) { |
|
|
lastError = error; |
|
|
const errorDetails = { |
|
|
attempt, |
|
|
maxRetries: this.maxRetries, |
|
|
endpoint, |
|
|
message: error.message, |
|
|
name: error.name, |
|
|
stack: error.stack |
|
|
}; |
|
|
|
|
|
console.warn(`[APIClient] Attempt ${attempt}/${this.maxRetries} failed for ${endpoint}:`, error.message); |
|
|
|
|
|
|
|
|
if (attempt === this.maxRetries) { |
|
|
console.error('[APIClient] All retries exhausted. Error details:', errorDetails); |
|
|
} |
|
|
|
|
|
if (attempt < this.maxRetries) { |
|
|
await this._sleep(this.retryDelay); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const duration = performance.now() - startTime; |
|
|
this._logError({ |
|
|
method, |
|
|
endpoint, |
|
|
message: lastError?.message || lastError?.toString() || 'Unknown error', |
|
|
duration: Math.round(duration), |
|
|
timestamp: Date.now(), |
|
|
}); |
|
|
|
|
|
this.pendingRequests.delete(cacheKey); |
|
|
|
|
|
|
|
|
return this._getFallbackData(endpoint, lastError); |
|
|
})(); |
|
|
|
|
|
this.pendingRequests.set(cacheKey, requestPromise); |
|
|
return requestPromise; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async get(endpoint, options = {}) { |
|
|
return this.request(endpoint, { ...options, method: 'GET' }); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async post(endpoint, data, options = {}) { |
|
|
return this.request(endpoint, { |
|
|
...options, |
|
|
method: 'POST', |
|
|
body: data, |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async put(endpoint, data, options = {}) { |
|
|
return this.request(endpoint, { |
|
|
...options, |
|
|
method: 'PUT', |
|
|
body: data, |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async delete(endpoint, options = {}) { |
|
|
return this.request(endpoint, { ...options, method: 'DELETE' }); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_getFromCache(key, ttl) { |
|
|
const cached = this.cache.get(key); |
|
|
|
|
|
if (!cached) return null; |
|
|
|
|
|
const now = Date.now(); |
|
|
const cacheTTL = ttl || this.cacheTTL; |
|
|
if (now - cached.timestamp > cacheTTL) { |
|
|
this.cache.delete(key); |
|
|
return null; |
|
|
} |
|
|
|
|
|
return cached.data; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_saveToCache(key, data, ttl) { |
|
|
this.cache.set(key, { |
|
|
data, |
|
|
timestamp: Date.now(), |
|
|
ttl: ttl || this.cacheTTL |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_buildURL(url, params) { |
|
|
if (!params || Object.keys(params).length === 0) return url; |
|
|
const searchParams = new URLSearchParams(); |
|
|
for (const [key, value] of Object.entries(params)) { |
|
|
if (value !== null && value !== undefined) { |
|
|
searchParams.append(key, String(value)); |
|
|
} |
|
|
} |
|
|
const queryString = searchParams.toString(); |
|
|
return queryString ? `${url}?${queryString}` : url; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_getCacheKey(url, params) { |
|
|
return params ? `${url}?${JSON.stringify(params)}` : url; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clearCache() { |
|
|
this.cache.clear(); |
|
|
console.log('[APIClient] Cache cleared'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clearCacheEntry(key) { |
|
|
const cacheKey = getCacheKey(key); |
|
|
this.cache.delete(cacheKey); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_logRequest(entry) { |
|
|
this.requestLog.unshift(entry); |
|
|
if (this.requestLog.length > this.maxLogSize) { |
|
|
this.requestLog.pop(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_logError(entry) { |
|
|
|
|
|
if (!entry.timestamp) { |
|
|
entry.timestamp = Date.now(); |
|
|
} |
|
|
|
|
|
|
|
|
entry.time = new Date(entry.timestamp).toISOString(); |
|
|
|
|
|
this.errorLog.unshift(entry); |
|
|
if (this.errorLog.length > this.maxLogSize) { |
|
|
this.errorLog.pop(); |
|
|
} |
|
|
|
|
|
|
|
|
console.error('[APIClient] Error logged:', { |
|
|
endpoint: entry.endpoint, |
|
|
method: entry.method, |
|
|
message: entry.message, |
|
|
duration: entry.duration |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getRequestLogs(limit = 20) { |
|
|
return this.requestLog.slice(0, limit); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getErrorLogs(limit = 20) { |
|
|
return this.errorLog.slice(0, limit); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_sleep(ms) { |
|
|
return new Promise(resolve => setTimeout(resolve, ms)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_getFallbackData(endpoint, error) { |
|
|
|
|
|
if (endpoint.includes('/resources/summary')) { |
|
|
return { |
|
|
success: false, |
|
|
error: error.message, |
|
|
summary: { |
|
|
total_resources: 0, |
|
|
free_resources: 0, |
|
|
models_available: 0, |
|
|
local_routes_count: 0, |
|
|
total_api_keys: 0, |
|
|
categories: {} |
|
|
}, |
|
|
fallback: true, |
|
|
timestamp: new Date().toISOString() |
|
|
}; |
|
|
} |
|
|
|
|
|
if (endpoint.includes('/models/status')) { |
|
|
return { |
|
|
success: false, |
|
|
error: error.message, |
|
|
status: 'error', |
|
|
status_message: `Error: ${error.message}`, |
|
|
models_loaded: 0, |
|
|
models_failed: 0, |
|
|
hf_mode: 'unknown', |
|
|
transformers_available: false, |
|
|
fallback: true, |
|
|
timestamp: new Date().toISOString() |
|
|
}; |
|
|
} |
|
|
|
|
|
if (endpoint.includes('/models/summary')) { |
|
|
return { |
|
|
ok: false, |
|
|
error: error.message, |
|
|
summary: { |
|
|
total_models: 0, |
|
|
loaded_models: 0, |
|
|
failed_models: 0, |
|
|
hf_mode: 'error', |
|
|
transformers_available: false |
|
|
}, |
|
|
categories: {}, |
|
|
health_registry: [], |
|
|
fallback: true, |
|
|
timestamp: new Date().toISOString() |
|
|
}; |
|
|
} |
|
|
|
|
|
if (endpoint.includes('/health') || endpoint.includes('/status')) { |
|
|
return { |
|
|
status: 'offline', |
|
|
healthy: false, |
|
|
error: error.message, |
|
|
fallback: true, |
|
|
timestamp: new Date().toISOString() |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
return { |
|
|
error: error.message, |
|
|
fallback: true, |
|
|
data: null, |
|
|
timestamp: new Date().toISOString() |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class CryptoMonitorAPI extends APIClient { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getHealth() { |
|
|
return this.get(API_ENDPOINTS.HEALTH); |
|
|
} |
|
|
|
|
|
async getStatus() { |
|
|
return this.get(API_ENDPOINTS.STATUS); |
|
|
} |
|
|
|
|
|
async getStats() { |
|
|
return this.get(API_ENDPOINTS.STATS); |
|
|
} |
|
|
|
|
|
async getResources() { |
|
|
return this.get(API_ENDPOINTS.RESOURCES); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getMarket() { |
|
|
return this.get(API_ENDPOINTS.MARKET); |
|
|
} |
|
|
|
|
|
async getTrending() { |
|
|
return this.get(API_ENDPOINTS.TRENDING); |
|
|
} |
|
|
|
|
|
async getSentiment() { |
|
|
return this.get(API_ENDPOINTS.SENTIMENT); |
|
|
} |
|
|
|
|
|
async getDefi() { |
|
|
return this.get(API_ENDPOINTS.DEFI); |
|
|
} |
|
|
|
|
|
async getTopCoins(limit = 50) { |
|
|
return this.get(`${API_ENDPOINTS.COINS_TOP}?limit=${limit}`); |
|
|
} |
|
|
|
|
|
async getCoinDetails(symbol) { |
|
|
return this.get(API_ENDPOINTS.COIN_DETAILS(symbol)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getPriceChart(symbol, timeframe = '7D') { |
|
|
return this.get(`${API_ENDPOINTS.PRICE_CHART(symbol)}?timeframe=${timeframe}`); |
|
|
} |
|
|
|
|
|
async analyzeChart(symbol, timeframe, indicators) { |
|
|
return this.post(API_ENDPOINTS.ANALYZE_CHART, { |
|
|
symbol, |
|
|
timeframe, |
|
|
indicators, |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getLatestNews(limit = 40) { |
|
|
return this.get(`${API_ENDPOINTS.NEWS_LATEST}?limit=${limit}`); |
|
|
} |
|
|
|
|
|
async analyzeNews(title, content) { |
|
|
return this.post(API_ENDPOINTS.NEWS_ANALYZE, { title, content }); |
|
|
} |
|
|
|
|
|
async summarizeNews(title, content) { |
|
|
return this.post(API_ENDPOINTS.NEWS_SUMMARIZE, { title, content }); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getModelsList() { |
|
|
return this.get(API_ENDPOINTS.MODELS_LIST); |
|
|
} |
|
|
|
|
|
async getModelsStatus() { |
|
|
return this.get(API_ENDPOINTS.MODELS_STATUS); |
|
|
} |
|
|
|
|
|
async getModelsStats() { |
|
|
return this.get(API_ENDPOINTS.MODELS_STATS); |
|
|
} |
|
|
|
|
|
async testModel(modelName, input) { |
|
|
return this.post(API_ENDPOINTS.MODELS_TEST, { |
|
|
model: modelName, |
|
|
input, |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async analyzeSentiment(text, mode = 'crypto', model = null) { |
|
|
return this.post(API_ENDPOINTS.SENTIMENT_ANALYZE, { |
|
|
text, |
|
|
mode, |
|
|
model, |
|
|
}); |
|
|
} |
|
|
|
|
|
async getGlobalSentiment() { |
|
|
return this.get(API_ENDPOINTS.SENTIMENT_GLOBAL); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getAIDecision(symbol, horizon, riskTolerance, context, model) { |
|
|
return this.post(API_ENDPOINTS.AI_DECISION, { |
|
|
symbol, |
|
|
horizon, |
|
|
risk_tolerance: riskTolerance, |
|
|
context, |
|
|
model, |
|
|
}); |
|
|
} |
|
|
|
|
|
async getAISignals(symbol) { |
|
|
return this.get(`${API_ENDPOINTS.AI_SIGNALS}?symbol=${symbol}`); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getDatasetsList() { |
|
|
return this.get(API_ENDPOINTS.DATASETS_LIST); |
|
|
} |
|
|
|
|
|
async previewDataset(name, limit = 10) { |
|
|
return this.get(`${API_ENDPOINTS.DATASET_PREVIEW(name)}?limit=${limit}`); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getProviders() { |
|
|
return this.get(API_ENDPOINTS.PROVIDERS); |
|
|
} |
|
|
|
|
|
async getProviderDetails(id) { |
|
|
return this.get(API_ENDPOINTS.PROVIDER_DETAILS(id)); |
|
|
} |
|
|
|
|
|
async checkProviderHealth(id) { |
|
|
return this.get(API_ENDPOINTS.PROVIDER_HEALTH(id)); |
|
|
} |
|
|
|
|
|
async getProvidersConfig() { |
|
|
return this.get(API_ENDPOINTS.PROVIDERS_CONFIG); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getLogs() { |
|
|
return this.get(API_ENDPOINTS.LOGS); |
|
|
} |
|
|
|
|
|
async getRecentLogs(limit = 50) { |
|
|
return this.get(`${API_ENDPOINTS.LOGS_RECENT}?limit=${limit}`); |
|
|
} |
|
|
|
|
|
async getErrorLogs(limit = 50) { |
|
|
return this.get(`${API_ENDPOINTS.LOGS_ERRORS}?limit=${limit}`); |
|
|
} |
|
|
|
|
|
async clearLogs() { |
|
|
return this.delete(API_ENDPOINTS.LOGS_CLEAR); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async runResourceDiscovery() { |
|
|
return this.post(API_ENDPOINTS.RESOURCES_DISCOVERY); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getHFHealth() { |
|
|
return this.get(API_ENDPOINTS.HF_HEALTH); |
|
|
} |
|
|
|
|
|
async runHFSentiment(text) { |
|
|
return this.post(API_ENDPOINTS.HF_RUN_SENTIMENT, { text }); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getFeatureFlags() { |
|
|
return this.get(API_ENDPOINTS.FEATURE_FLAGS); |
|
|
} |
|
|
|
|
|
async updateFeatureFlag(name, value) { |
|
|
return this.put(API_ENDPOINTS.FEATURE_FLAG_UPDATE(name), { value }); |
|
|
} |
|
|
|
|
|
async resetFeatureFlags() { |
|
|
return this.post(API_ENDPOINTS.FEATURE_FLAGS_RESET); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getSettings() { |
|
|
return this.get(API_ENDPOINTS.SETTINGS); |
|
|
} |
|
|
|
|
|
async saveTokens(tokens) { |
|
|
return this.post(API_ENDPOINTS.SETTINGS_TOKENS, tokens); |
|
|
} |
|
|
|
|
|
async saveTelegramSettings(settings) { |
|
|
return this.post(API_ENDPOINTS.SETTINGS_TELEGRAM, settings); |
|
|
} |
|
|
|
|
|
async saveSignalSettings(settings) { |
|
|
return this.post(API_ENDPOINTS.SETTINGS_SIGNALS, settings); |
|
|
} |
|
|
|
|
|
async saveSchedulingSettings(settings) { |
|
|
return this.post(API_ENDPOINTS.SETTINGS_SCHEDULING, settings); |
|
|
} |
|
|
|
|
|
async saveNotificationSettings(settings) { |
|
|
return this.post(API_ENDPOINTS.SETTINGS_NOTIFICATIONS, settings); |
|
|
} |
|
|
|
|
|
async saveAppearanceSettings(settings) { |
|
|
return this.post(API_ENDPOINTS.SETTINGS_APPEARANCE, settings); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const api = new CryptoMonitorAPI(); |
|
|
export default api; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const apiClient = { |
|
|
async fetch(url, options = {}) { |
|
|
|
|
|
const method = (options.method || 'GET').toUpperCase(); |
|
|
const endpoint = url.replace(/^.*\/api/, '/api'); |
|
|
|
|
|
try { |
|
|
let data; |
|
|
if (method === 'GET') { |
|
|
data = await api.get(endpoint, { skipCache: options.skipCache, forceRefresh: options.forceRefresh }); |
|
|
} else if (method === 'POST') { |
|
|
const body = options.body ? (typeof options.body === 'string' ? JSON.parse(options.body) : options.body) : {}; |
|
|
data = await api.post(endpoint, body); |
|
|
} else if (method === 'PUT') { |
|
|
const body = options.body ? (typeof options.body === 'string' ? JSON.parse(options.body) : options.body) : {}; |
|
|
data = await api.put(endpoint, body); |
|
|
} else if (method === 'DELETE') { |
|
|
data = await api.delete(endpoint); |
|
|
} else { |
|
|
data = await api.get(endpoint); |
|
|
} |
|
|
|
|
|
|
|
|
return new Response(JSON.stringify(data), { |
|
|
status: 200, |
|
|
statusText: 'OK', |
|
|
headers: { 'Content-Type': 'application/json' } |
|
|
}); |
|
|
} catch (error) { |
|
|
|
|
|
return new Response(JSON.stringify({ |
|
|
error: error.message || 'Request failed', |
|
|
success: false |
|
|
}), { |
|
|
status: error.status || 500, |
|
|
statusText: error.statusText || 'Internal Server Error', |
|
|
headers: { 'Content-Type': 'application/json' } |
|
|
}); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
console.log('[APIClient] Initialized (HTTP-only, no WebSocket)'); |
|
|
|