| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import { CONFIG, API_ENDPOINTS, buildApiUrl, getCacheKey } from './config.js';
|
|
|
| |
| |
|
|
| class APIClient {
|
| constructor(baseURL = CONFIG.API_BASE_URL) {
|
| this.baseURL = baseURL;
|
| this.cache = new Map();
|
| this.cacheTTL = CONFIG.CACHE_TTL;
|
| this.maxRetries = CONFIG.MAX_RETRIES;
|
| this.retryDelay = CONFIG.RETRY_DELAY;
|
| this.requestLog = [];
|
| this.errorLog = [];
|
| this.maxLogSize = 100;
|
| }
|
|
|
| |
| |
|
|
| async request(endpoint, options = {}) {
|
| const url = `${this.baseURL}${endpoint}`;
|
| const method = options.method || 'GET';
|
| 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(endpoint);
|
| if (cached) {
|
| console.log(`[APIClient] Cache hit: ${endpoint}`);
|
| return cached;
|
| }
|
| }
|
| }
|
|
|
|
|
| let lastError;
|
| for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
| try {
|
| const response = await fetch(url, {
|
| 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(endpoint, data);
|
| }
|
|
|
|
|
| this._logRequest({
|
| method,
|
| endpoint,
|
| status: response.status,
|
| duration: Math.round(duration),
|
| timestamp: Date.now(),
|
| });
|
|
|
| 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(),
|
| });
|
|
|
|
|
| return this._getFallbackData(endpoint, lastError);
|
| }
|
|
|
| |
| |
|
|
| 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) {
|
| const cacheKey = getCacheKey(key);
|
| const cached = this.cache.get(cacheKey);
|
|
|
| if (!cached) return null;
|
|
|
| const now = Date.now();
|
| if (now - cached.timestamp > this.cacheTTL) {
|
| this.cache.delete(cacheKey);
|
| return null;
|
| }
|
|
|
| return cached.data;
|
| }
|
|
|
| |
| |
|
|
| _saveToCache(key, data) {
|
| const cacheKey = getCacheKey(key);
|
| this.cache.set(cacheKey, {
|
| data,
|
| timestamp: Date.now(),
|
| });
|
| }
|
|
|
| |
| |
|
|
| 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)');
|
|
|