Your Name
feat: UI improvements and error suppression - Enhanced dashboard and market pages with improved header buttons, logo, and currency symbol display - Stopped animated ticker - Removed pie chart legends - Added error suppressor for external service errors (SSE, Permissions-Policy warnings) - Improved header button prominence and icon appearance - Enhanced logo with glow effects and better design - Fixed currency symbol visibility in market tables
8b7b267
| /** | |
| * Enhanced Notification System | |
| * Multi-channel notifications with retry logic | |
| * Supports: Telegram, Email, Browser Push, WebSocket | |
| */ | |
| /** | |
| * Notification priorities | |
| */ | |
| export const NOTIFICATION_PRIORITY = { | |
| LOW: 'low', | |
| MEDIUM: 'medium', | |
| HIGH: 'high', | |
| URGENT: 'urgent' | |
| }; | |
| /** | |
| * Notification channels | |
| */ | |
| export const NOTIFICATION_CHANNELS = { | |
| TELEGRAM: 'telegram', | |
| EMAIL: 'email', | |
| BROWSER: 'browser', | |
| WEBSOCKET: 'websocket' | |
| }; | |
| /** | |
| * Enhanced Notification Manager | |
| */ | |
| export class NotificationManager { | |
| constructor(config = {}) { | |
| this.enabled = config.enabled !== false; | |
| this.channels = config.channels || ['browser']; | |
| this.telegramConfig = config.telegram || null; | |
| this.emailConfig = config.email || null; | |
| this.retryAttempts = config.retryAttempts || 3; | |
| this.retryDelay = config.retryDelay || 5000; | |
| this.queue = []; | |
| this.processing = false; | |
| this.sent = []; | |
| this.failed = []; | |
| this.rateLimit = { | |
| maxPerMinute: 10, | |
| count: 0, | |
| resetTime: Date.now() + 60000 | |
| }; | |
| } | |
| /** | |
| * Send notification to all configured channels | |
| * @param {Object} notification - Notification object | |
| * @returns {Promise<Object>} Results from all channels | |
| */ | |
| async send(notification) { | |
| if (!this.enabled) { | |
| console.log('[NotificationManager] Notifications disabled'); | |
| return { success: false, reason: 'disabled' }; | |
| } | |
| // Check rate limiting | |
| if (!this.checkRateLimit()) { | |
| console.warn('[NotificationManager] Rate limit exceeded'); | |
| this.queue.push(notification); | |
| return { success: false, reason: 'rate_limited', queued: true }; | |
| } | |
| // Validate notification | |
| const validated = this.validateNotification(notification); | |
| if (!validated.valid) { | |
| return { success: false, reason: validated.error }; | |
| } | |
| // Enrich notification | |
| const enriched = this.enrichNotification(notification); | |
| // Send to all channels | |
| const results = {}; | |
| for (const channel of this.channels) { | |
| try { | |
| results[channel] = await this.sendToChannel(enriched, channel); | |
| } catch (error) { | |
| console.error(`[NotificationManager] ${channel} error:`, error); | |
| results[channel] = { success: false, error: error.message }; | |
| } | |
| } | |
| // Log results | |
| if (Object.values(results).some(r => r.success)) { | |
| this.sent.push({ ...enriched, timestamp: Date.now(), results }); | |
| } else { | |
| this.failed.push({ ...enriched, timestamp: Date.now(), results }); | |
| } | |
| return { success: true, results }; | |
| } | |
| /** | |
| * Send trading signal notification | |
| * @param {Object} signal - Trading signal | |
| * @returns {Promise<Object>} Send results | |
| */ | |
| async sendSignal(signal) { | |
| const priority = this.determineSignalPriority(signal); | |
| const notification = { | |
| type: 'signal', | |
| priority, | |
| title: `🚨 ${signal.strategy} - ${signal.signal.toUpperCase()}`, | |
| message: this.formatSignalMessage(signal), | |
| data: signal, | |
| action: { | |
| label: 'View Analysis', | |
| url: `/trading-assistant?symbol=${signal.symbol || 'BTC'}` | |
| } | |
| }; | |
| return this.send(notification); | |
| } | |
| /** | |
| * Send error notification | |
| * @param {Error} error - Error object | |
| * @param {string} context - Error context | |
| * @returns {Promise<Object>} Send results | |
| */ | |
| async sendError(error, context = 'Unknown') { | |
| const notification = { | |
| type: 'error', | |
| priority: NOTIFICATION_PRIORITY.HIGH, | |
| title: `⚠️ Error: ${context}`, | |
| message: `${error.message}\n\nTime: ${new Date().toLocaleString()}`, | |
| data: { error: error.message, stack: error.stack, context } | |
| }; | |
| return this.send(notification); | |
| } | |
| /** | |
| * Send price alert notification | |
| * @param {Object} alert - Price alert | |
| * @returns {Promise<Object>} Send results | |
| */ | |
| async sendPriceAlert(alert) { | |
| const notification = { | |
| type: 'price_alert', | |
| priority: NOTIFICATION_PRIORITY.MEDIUM, | |
| title: `💰 Price Alert: ${alert.symbol}`, | |
| message: `${alert.symbol} reached ${alert.targetPrice}\nCurrent: $${alert.currentPrice.toFixed(2)}`, | |
| data: alert | |
| }; | |
| return this.send(notification); | |
| } | |
| /** | |
| * Send to specific channel | |
| * @param {Object} notification - Notification | |
| * @param {string} channel - Channel name | |
| * @returns {Promise<Object>} Channel result | |
| */ | |
| async sendToChannel(notification, channel) { | |
| const handlers = { | |
| [NOTIFICATION_CHANNELS.TELEGRAM]: () => this.sendTelegram(notification), | |
| [NOTIFICATION_CHANNELS.EMAIL]: () => this.sendEmail(notification), | |
| [NOTIFICATION_CHANNELS.BROWSER]: () => this.sendBrowser(notification), | |
| [NOTIFICATION_CHANNELS.WEBSOCKET]: () => this.sendWebSocket(notification) | |
| }; | |
| const handler = handlers[channel]; | |
| if (!handler) { | |
| throw new Error(`Unknown channel: ${channel}`); | |
| } | |
| return this.retryOperation(() => handler(), this.retryAttempts); | |
| } | |
| /** | |
| * Send via Telegram | |
| * @param {Object} notification - Notification | |
| * @returns {Promise<Object>} Result | |
| */ | |
| async sendTelegram(notification) { | |
| if (!this.telegramConfig || !this.telegramConfig.botToken || !this.telegramConfig.chatId) { | |
| return { success: false, error: 'Telegram not configured' }; | |
| } | |
| const message = this.formatTelegramMessage(notification); | |
| try { | |
| // Validate Telegram config | |
| if (!this.telegramConfig.botToken || typeof this.telegramConfig.botToken !== 'string') { | |
| return { success: false, error: 'Invalid bot token' }; | |
| } | |
| if (!this.telegramConfig.chatId || (typeof this.telegramConfig.chatId !== 'string' && typeof this.telegramConfig.chatId !== 'number')) { | |
| return { success: false, error: 'Invalid chat ID' }; | |
| } | |
| const response = await fetch( | |
| `https://api.telegram.org/bot${this.telegramConfig.botToken}/sendMessage`, | |
| { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| chat_id: this.telegramConfig.chatId, | |
| text: message, | |
| parse_mode: 'HTML', | |
| disable_web_page_preview: true | |
| }), | |
| signal: AbortSignal.timeout(10000) | |
| } | |
| ); | |
| const data = await response.json(); | |
| if (data.ok) { | |
| return { success: true, messageId: data.result.message_id }; | |
| } else { | |
| return { success: false, error: data.description }; | |
| } | |
| } catch (error) { | |
| return { success: false, error: error.message }; | |
| } | |
| } | |
| /** | |
| * Send via Email (requires backend) | |
| * @param {Object} notification - Notification | |
| * @returns {Promise<Object>} Result | |
| */ | |
| async sendEmail(notification) { | |
| if (!this.emailConfig || !this.emailConfig.to) { | |
| return { success: false, error: 'Email not configured' }; | |
| } | |
| // Validate email config | |
| if (typeof this.emailConfig.to !== 'string' || this.emailConfig.to.length === 0) { | |
| return { success: false, error: 'Invalid email address' }; | |
| } | |
| const baseUrl = window.location.origin; // Use relative URL for Hugging Face compatibility | |
| try { | |
| const response = await fetch(`${baseUrl}/api/notifications/email`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| to: this.emailConfig.to, | |
| subject: notification.title || 'Notification', | |
| body: notification.message || '', | |
| data: notification.data || {} | |
| }), | |
| signal: AbortSignal.timeout(10000) | |
| }); | |
| if (response.ok) { | |
| return { success: true }; | |
| } else { | |
| return { success: false, error: `HTTP ${response.status}` }; | |
| } | |
| } catch (error) { | |
| return { success: false, error: error.message }; | |
| } | |
| } | |
| /** | |
| * Send browser notification | |
| * @param {Object} notification - Notification | |
| * @returns {Promise<Object>} Result | |
| */ | |
| async sendBrowser(notification) { | |
| // Check if browser notifications are supported | |
| if (!('Notification' in window)) { | |
| return { success: false, error: 'Browser notifications not supported' }; | |
| } | |
| // Request permission if needed | |
| if (Notification.permission === 'default') { | |
| const permission = await Notification.requestPermission(); | |
| if (permission !== 'granted') { | |
| return { success: false, error: 'Permission denied' }; | |
| } | |
| } | |
| if (Notification.permission !== 'granted') { | |
| return { success: false, error: 'Permission denied' }; | |
| } | |
| try { | |
| const notif = new Notification(notification.title, { | |
| body: notification.message, | |
| icon: '/static/images/logo.png', | |
| badge: '/static/images/badge.png', | |
| tag: `${notification.type}-${Date.now()}`, | |
| requireInteraction: notification.priority === NOTIFICATION_PRIORITY.URGENT, | |
| silent: notification.priority === NOTIFICATION_PRIORITY.LOW | |
| }); | |
| if (notification.action) { | |
| notif.onclick = () => { | |
| window.focus(); | |
| if (notification.action.url) { | |
| window.location.href = notification.action.url; | |
| } | |
| notif.close(); | |
| }; | |
| } | |
| return { success: true }; | |
| } catch (error) { | |
| return { success: false, error: error.message }; | |
| } | |
| } | |
| /** | |
| * Send via WebSocket | |
| * @param {Object} notification - Notification | |
| * @returns {Promise<Object>} Result | |
| */ | |
| async sendWebSocket(notification) { | |
| // This would connect to a WebSocket server for real-time delivery | |
| // For now, we'll use window events as a fallback | |
| try { | |
| window.dispatchEvent(new CustomEvent('notification', { | |
| detail: notification | |
| })); | |
| return { success: true }; | |
| } catch (error) { | |
| return { success: false, error: error.message }; | |
| } | |
| } | |
| /** | |
| * Format Telegram message | |
| * @param {Object} notification - Notification | |
| * @returns {string} Formatted message | |
| */ | |
| formatTelegramMessage(notification) { | |
| let message = `<b>${this.escapeHtml(notification.title)}</b>\n\n`; | |
| message += `${this.escapeHtml(notification.message)}\n\n`; | |
| if (notification.data) { | |
| if (notification.data.entry) { | |
| message += `<b>Entry:</b> $${notification.data.entry.toFixed(2)}\n`; | |
| } | |
| if (notification.data.stopLoss) { | |
| message += `<b>Stop Loss:</b> $${notification.data.stopLoss.toFixed(2)}\n`; | |
| } | |
| if (notification.data.targets && notification.data.targets.length > 0) { | |
| message += `<b>Targets:</b>\n`; | |
| notification.data.targets.forEach((t, i) => { | |
| message += ` TP${i + 1}: $${t.level.toFixed(2)} (${t.percentage}%)\n`; | |
| }); | |
| } | |
| if (notification.data.confidence) { | |
| message += `\n<b>Confidence:</b> ${notification.data.confidence.toFixed(0)}%\n`; | |
| } | |
| } | |
| message += `\n<i>${new Date().toLocaleString()}</i>`; | |
| return message; | |
| } | |
| /** | |
| * Format signal message | |
| * @param {Object} signal - Trading signal | |
| * @returns {string} Formatted message | |
| */ | |
| formatSignalMessage(signal) { | |
| let message = `Signal: ${signal.signal.toUpperCase()}\n`; | |
| message += `Strategy: ${signal.strategy}\n`; | |
| message += `Confidence: ${signal.confidence?.toFixed(0) || 0}%\n\n`; | |
| if (signal.entry) { | |
| message += `Entry: $${signal.entry.toFixed(2)}\n`; | |
| } | |
| if (signal.stopLoss) { | |
| message += `Stop Loss: $${signal.stopLoss.toFixed(2)}\n`; | |
| } | |
| if (signal.targets && signal.targets.length > 0) { | |
| message += `\nTargets:\n`; | |
| signal.targets.forEach((t, i) => { | |
| message += ` TP${i + 1}: $${t.level.toFixed(2)}\n`; | |
| }); | |
| } | |
| if (signal.riskRewardRatio) { | |
| message += `\nRisk/Reward: ${signal.riskRewardRatio}`; | |
| } | |
| return message; | |
| } | |
| /** | |
| * Determine signal priority | |
| * @param {Object} signal - Trading signal | |
| * @returns {string} Priority level | |
| */ | |
| determineSignalPriority(signal) { | |
| const confidence = signal.confidence || 0; | |
| if (confidence >= 90 && signal.signal !== 'hold') { | |
| return NOTIFICATION_PRIORITY.URGENT; | |
| } else if (confidence >= 75 && signal.signal !== 'hold') { | |
| return NOTIFICATION_PRIORITY.HIGH; | |
| } else if (signal.signal !== 'hold') { | |
| return NOTIFICATION_PRIORITY.MEDIUM; | |
| } else { | |
| return NOTIFICATION_PRIORITY.LOW; | |
| } | |
| } | |
| /** | |
| * Validate notification | |
| * @param {Object} notification - Notification | |
| * @returns {Object} Validation result | |
| */ | |
| validateNotification(notification) { | |
| if (!notification) { | |
| return { valid: false, error: 'Notification is null' }; | |
| } | |
| if (!notification.title || typeof notification.title !== 'string') { | |
| return { valid: false, error: 'Invalid title' }; | |
| } | |
| if (!notification.message || typeof notification.message !== 'string') { | |
| return { valid: false, error: 'Invalid message' }; | |
| } | |
| return { valid: true }; | |
| } | |
| /** | |
| * Enrich notification with metadata | |
| * @param {Object} notification - Notification | |
| * @returns {Object} Enriched notification | |
| */ | |
| enrichNotification(notification) { | |
| return { | |
| ...notification, | |
| id: this.generateId(), | |
| timestamp: Date.now(), | |
| priority: notification.priority || NOTIFICATION_PRIORITY.MEDIUM, | |
| type: notification.type || 'info' | |
| }; | |
| } | |
| /** | |
| * Check rate limiting | |
| * @returns {boolean} Whether sending is allowed | |
| */ | |
| checkRateLimit() { | |
| const now = Date.now(); | |
| if (now >= this.rateLimit.resetTime) { | |
| this.rateLimit.count = 0; | |
| this.rateLimit.resetTime = now + 60000; | |
| } | |
| if (this.rateLimit.count >= this.rateLimit.maxPerMinute) { | |
| return false; | |
| } | |
| this.rateLimit.count++; | |
| return true; | |
| } | |
| /** | |
| * Retry operation with exponential backoff | |
| * @param {Function} operation - Operation to retry | |
| * @param {number} attempts - Number of attempts | |
| * @returns {Promise<any>} Operation result | |
| */ | |
| async retryOperation(operation, attempts) { | |
| for (let i = 0; i < attempts; i++) { | |
| try { | |
| return await operation(); | |
| } catch (error) { | |
| if (i === attempts - 1) { | |
| throw error; | |
| } | |
| const delay = this.retryDelay * Math.pow(2, i); | |
| console.log(`[NotificationManager] Retry ${i + 1}/${attempts} after ${delay}ms`); | |
| await new Promise(resolve => setTimeout(resolve, delay)); | |
| } | |
| } | |
| } | |
| /** | |
| * Process queued notifications | |
| */ | |
| async processQueue() { | |
| if (this.processing || this.queue.length === 0) { | |
| return; | |
| } | |
| this.processing = true; | |
| while (this.queue.length > 0) { | |
| if (!this.checkRateLimit()) { | |
| await new Promise(resolve => setTimeout(resolve, 10000)); | |
| continue; | |
| } | |
| const notification = this.queue.shift(); | |
| await this.send(notification); | |
| } | |
| this.processing = false; | |
| } | |
| /** | |
| * Escape HTML for Telegram | |
| * @param {string} text - Text to escape | |
| * @returns {string} Escaped text | |
| */ | |
| escapeHtml(text) { | |
| const map = { | |
| '&': '&', | |
| '<': '<', | |
| '>': '>', | |
| '"': '"', | |
| "'": ''' | |
| }; | |
| return text.replace(/[&<>"']/g, m => map[m]); | |
| } | |
| /** | |
| * Generate unique ID | |
| * @returns {string} Unique ID | |
| */ | |
| generateId() { | |
| return `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; | |
| } | |
| /** | |
| * Get notification history | |
| * @param {number} limit - Maximum number of notifications | |
| * @returns {Array<Object>} Recent notifications | |
| */ | |
| getHistory(limit = 50) { | |
| return this.sent.slice(-limit).reverse(); | |
| } | |
| /** | |
| * Get failed notifications | |
| * @returns {Array<Object>} Failed notifications | |
| */ | |
| getFailed() { | |
| return this.failed.slice(-20).reverse(); | |
| } | |
| /** | |
| * Clear history | |
| */ | |
| clearHistory() { | |
| this.sent = []; | |
| this.failed = []; | |
| } | |
| /** | |
| * Update configuration | |
| * @param {Object} config - New configuration | |
| */ | |
| updateConfig(config) { | |
| if (config.enabled !== undefined) { | |
| this.enabled = config.enabled; | |
| } | |
| if (config.channels) { | |
| this.channels = config.channels; | |
| } | |
| if (config.telegram) { | |
| this.telegramConfig = config.telegram; | |
| } | |
| if (config.email) { | |
| this.emailConfig = config.email; | |
| } | |
| } | |
| /** | |
| * Test notification system | |
| * @returns {Promise<Object>} Test results | |
| */ | |
| async test() { | |
| const testNotification = { | |
| type: 'test', | |
| priority: NOTIFICATION_PRIORITY.LOW, | |
| title: '✅ Test Notification', | |
| message: 'This is a test notification from the Enhanced Notification System', | |
| data: { test: true, timestamp: Date.now() } | |
| }; | |
| return this.send(testNotification); | |
| } | |
| } | |
| export default NotificationManager; | |