|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)');
|
|
|
|