/** * Settings Page - Functional Implementation * Manages all application settings with local storage persistence */ import { api } from '../../shared/js/core/api-client.js'; import { LayoutManager } from '../../shared/js/core/layout-manager.js'; import { Toast } from '../../shared/js/components/toast.js'; // Default settings const DEFAULT_SETTINGS = { tokens: { hfToken: '', coingeckoKey: '', cmcKey: '', etherscanKey: '', cryptocompareKey: '', }, telegram: { botToken: '', chatId: '', enabled: true, silent: false, includeCharts: true, }, signals: { bullish: true, bearish: true, whale: true, news: false, sentiment: true, price: true, confidenceThreshold: 70, priceChangeThreshold: 5, whaleThreshold: 100000, watchedCoins: 'BTC, ETH, SOL', }, scheduling: { autoRefreshEnabled: true, intervalMarket: 30, intervalNews: 120, intervalSentiment: 300, intervalWhale: 60, intervalBlockchain: 300, intervalModels: 600, quietHoursEnabled: false, quietStart: '22:00', quietEnd: '08:00', }, notifications: { browser: true, sound: true, toast: true, soundType: 'default', volume: 50, }, appearance: { theme: 'dark', compactMode: false, showAnimations: true, showBgEffects: true, }, }; const STORAGE_KEY = 'crypto_monitor_settings'; class SettingsPage { constructor() { this.settings = this.loadSettings(); this.activeSection = 'api-tokens'; } async init() { try { await LayoutManager.injectLayouts(); LayoutManager.setActiveNav('settings'); this.bindEvents(); this.populateForm(); this.applySettings(); } catch (error) { console.error('[Settings] Init error:', error); Toast.error('Failed to initialize settings page'); } } loadSettings() { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved); // Merge with defaults to ensure all keys exist return this.deepMerge(DEFAULT_SETTINGS, parsed); } } catch (error) { console.warn('[Settings] Could not load settings:', error); } return { ...DEFAULT_SETTINGS }; } saveSettings() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(this.settings)); return true; } catch (error) { console.error('[Settings] Could not save settings:', error); return false; } } deepMerge(target, source) { const result = { ...target }; for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { result[key] = this.deepMerge(target[key] || {}, source[key]); } else { result[key] = source[key]; } } return result; } bindEvents() { // Navigation buttons document.querySelectorAll('.settings-nav-btn').forEach(btn => { btn.addEventListener('click', (e) => this.switchSection(e.target.closest('.settings-nav-btn').dataset.section)); }); // Save all button document.getElementById('save-all-btn')?.addEventListener('click', () => this.saveAllSettings()); // Reset button document.getElementById('reset-btn')?.addEventListener('click', () => this.resetSettings()); // Toggle visibility buttons document.querySelectorAll('.toggle-visibility').forEach(btn => { btn.addEventListener('click', (e) => { const targetId = e.target.closest('.toggle-visibility').dataset.target; this.togglePasswordVisibility(targetId); }); }); // Range inputs with value display this.bindRangeInput('signal-confidence', 'confidence-value', '%'); this.bindRangeInput('price-change-threshold', 'price-threshold-value', '%'); this.bindRangeInput('notif-volume', 'volume-value', '%'); // Section-specific save buttons document.getElementById('save-tokens-btn')?.addEventListener('click', () => this.saveTokens()); document.getElementById('test-tokens-btn')?.addEventListener('click', () => this.testTokens()); document.getElementById('save-telegram-btn')?.addEventListener('click', () => this.saveTelegram()); document.getElementById('test-telegram-btn')?.addEventListener('click', () => this.testTelegram()); document.getElementById('save-signals-btn')?.addEventListener('click', () => this.saveSignals()); document.getElementById('save-scheduling-btn')?.addEventListener('click', () => this.saveScheduling()); document.getElementById('save-notif-btn')?.addEventListener('click', () => this.saveNotifications()); document.getElementById('test-notif-btn')?.addEventListener('click', () => this.testNotification()); document.getElementById('save-appearance-btn')?.addEventListener('click', () => this.saveAppearance()); // Theme radio buttons document.querySelectorAll('input[name="theme"]').forEach(radio => { radio.addEventListener('change', (e) => { this.settings.appearance.theme = e.target.value; this.applyTheme(); }); }); // Auto-save toggle changes document.querySelectorAll('.toggle-switch input').forEach(toggle => { toggle.addEventListener('change', () => this.handleToggleChange(toggle)); }); } bindRangeInput(rangeId, valueId, suffix = '') { const range = document.getElementById(rangeId); const valueEl = document.getElementById(valueId); if (range && valueEl) { range.addEventListener('input', () => { valueEl.textContent = `${range.value}${suffix}`; }); } } switchSection(sectionId) { // Update nav buttons document.querySelectorAll('.settings-nav-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.section === sectionId); }); // Update sections document.querySelectorAll('.settings-section').forEach(section => { section.classList.toggle('active', section.id === `section-${sectionId}`); }); this.activeSection = sectionId; } populateForm() { // API Tokens this.setInputValue('hf-token', this.settings.tokens.hfToken); this.setInputValue('coingecko-key', this.settings.tokens.coingeckoKey); this.setInputValue('cmc-key', this.settings.tokens.cmcKey); this.setInputValue('etherscan-key', this.settings.tokens.etherscanKey); this.setInputValue('cryptocompare-key', this.settings.tokens.cryptocompareKey); // Telegram this.setInputValue('telegram-bot-token', this.settings.telegram.botToken); this.setInputValue('telegram-chat-id', this.settings.telegram.chatId); this.setCheckbox('telegram-enabled', this.settings.telegram.enabled); this.setCheckbox('telegram-silent', this.settings.telegram.silent); this.setCheckbox('telegram-charts', this.settings.telegram.includeCharts); // Signals this.setCheckbox('signal-bullish', this.settings.signals.bullish); this.setCheckbox('signal-bearish', this.settings.signals.bearish); this.setCheckbox('signal-whale', this.settings.signals.whale); this.setCheckbox('signal-news', this.settings.signals.news); this.setCheckbox('signal-sentiment', this.settings.signals.sentiment); this.setCheckbox('signal-price', this.settings.signals.price); this.setRangeValue('signal-confidence', this.settings.signals.confidenceThreshold, 'confidence-value', '%'); this.setRangeValue('price-change-threshold', this.settings.signals.priceChangeThreshold, 'price-threshold-value', '%'); this.setInputValue('whale-threshold', this.settings.signals.whaleThreshold); this.setInputValue('watched-coins', this.settings.signals.watchedCoins); // Scheduling this.setCheckbox('auto-refresh-enabled', this.settings.scheduling.autoRefreshEnabled); this.setSelectValue('interval-market', this.settings.scheduling.intervalMarket); this.setSelectValue('interval-news', this.settings.scheduling.intervalNews); this.setSelectValue('interval-sentiment', this.settings.scheduling.intervalSentiment); this.setSelectValue('interval-whale', this.settings.scheduling.intervalWhale); this.setSelectValue('interval-blockchain', this.settings.scheduling.intervalBlockchain); this.setSelectValue('interval-models', this.settings.scheduling.intervalModels); this.setCheckbox('quiet-hours-enabled', this.settings.scheduling.quietHoursEnabled); this.setInputValue('quiet-start', this.settings.scheduling.quietStart); this.setInputValue('quiet-end', this.settings.scheduling.quietEnd); // Notifications this.setCheckbox('notif-browser', this.settings.notifications.browser); this.setCheckbox('notif-sound', this.settings.notifications.sound); this.setCheckbox('notif-toast', this.settings.notifications.toast); this.setSelectValue('notif-sound-type', this.settings.notifications.soundType); this.setRangeValue('notif-volume', this.settings.notifications.volume, 'volume-value', '%'); // Appearance this.setRadioValue('theme', this.settings.appearance.theme); this.setCheckbox('compact-mode', this.settings.appearance.compactMode); this.setCheckbox('show-animations', this.settings.appearance.showAnimations); this.setCheckbox('show-bg-effects', this.settings.appearance.showBgEffects); } // Helper methods for form population setInputValue(id, value) { const el = document.getElementById(id); if (el) el.value = value || ''; } setCheckbox(id, checked) { const el = document.getElementById(id); if (el) el.checked = checked; } setSelectValue(id, value) { const el = document.getElementById(id); if (el) el.value = value; } setRadioValue(name, value) { const radio = document.querySelector(`input[name="${name}"][value="${value}"]`); if (radio) radio.checked = true; } setRangeValue(id, value, valueDisplayId, suffix = '') { const range = document.getElementById(id); const valueDisplay = document.getElementById(valueDisplayId); if (range) range.value = value; if (valueDisplay) valueDisplay.textContent = `${value}${suffix}`; } togglePasswordVisibility(inputId) { const input = document.getElementById(inputId); if (input) { input.type = input.type === 'password' ? 'text' : 'password'; } } handleToggleChange(toggle) { // Auto-apply certain toggles immediately if (toggle.id === 'show-animations') { this.applyAnimations(toggle.checked); } else if (toggle.id === 'show-bg-effects') { this.applyBgEffects(toggle.checked); } } // Save methods saveTokens() { this.settings.tokens = { hfToken: document.getElementById('hf-token')?.value || '', coingeckoKey: document.getElementById('coingecko-key')?.value || '', cmcKey: document.getElementById('cmc-key')?.value || '', etherscanKey: document.getElementById('etherscan-key')?.value || '', cryptocompareKey: document.getElementById('cryptocompare-key')?.value || '', }; if (this.saveSettings()) { Toast.success('API tokens saved successfully'); this.sendTokensToBackend(); } else { Toast.error('Failed to save tokens'); } } async sendTokensToBackend() { try { await api.post('/settings/tokens', this.settings.tokens); } catch (error) { console.warn('[Settings] Could not sync tokens with backend:', error); } } async testTokens() { Toast.info('Testing API tokens...'); const results = []; // Test HuggingFace if (this.settings.tokens.hfToken) { try { const response = await fetch('https://huggingface.co/api/whoami-v2', { headers: { 'Authorization': `Bearer ${this.settings.tokens.hfToken}` } }); results.push({ name: 'HuggingFace', ok: response.ok }); } catch { results.push({ name: 'HuggingFace', ok: false }); } } // Test CoinGecko if (this.settings.tokens.coingeckoKey) { try { const response = await fetch(`https://api.coingecko.com/api/v3/ping?x_cg_demo_api_key=${this.settings.tokens.coingeckoKey}`); results.push({ name: 'CoinGecko', ok: response.ok }); } catch { results.push({ name: 'CoinGecko', ok: false }); } } // Show results const passed = results.filter(r => r.ok).length; const total = results.length; if (total === 0) { Toast.warning('No tokens configured to test'); } else if (passed === total) { Toast.success(`All ${total} tokens verified successfully`); } else { Toast.warning(`${passed}/${total} tokens verified`); } } saveTelegram() { this.settings.telegram = { botToken: document.getElementById('telegram-bot-token')?.value || '', chatId: document.getElementById('telegram-chat-id')?.value || '', enabled: document.getElementById('telegram-enabled')?.checked || false, silent: document.getElementById('telegram-silent')?.checked || false, includeCharts: document.getElementById('telegram-charts')?.checked || false, }; if (this.saveSettings()) { Toast.success('Telegram settings saved'); this.sendTelegramToBackend(); } else { Toast.error('Failed to save Telegram settings'); } } async sendTelegramToBackend() { try { await api.post('/settings/telegram', this.settings.telegram); } catch (error) { console.warn('[Settings] Could not sync Telegram settings with backend:', error); } } async testTelegram() { const botToken = document.getElementById('telegram-bot-token')?.value; const chatId = document.getElementById('telegram-chat-id')?.value; if (!botToken || !chatId) { Toast.warning('Please enter both bot token and chat ID'); return; } Toast.info('Sending test message...'); try { const message = `🚀 *Crypto Monitor ULTIMATE*\n\nTest message sent successfully!\n\n_Time: ${new Date().toLocaleString()}_`; const response = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text: message, parse_mode: 'Markdown', disable_notification: document.getElementById('telegram-silent')?.checked || false, }), }); const data = await response.json(); if (data.ok) { Toast.success('Test message sent successfully! Check your Telegram.'); } else { Toast.error(`Telegram error: ${data.description}`); } } catch (error) { Toast.error(`Failed to send test message: ${error.message}`); } } saveSignals() { this.settings.signals = { bullish: document.getElementById('signal-bullish')?.checked || false, bearish: document.getElementById('signal-bearish')?.checked || false, whale: document.getElementById('signal-whale')?.checked || false, news: document.getElementById('signal-news')?.checked || false, sentiment: document.getElementById('signal-sentiment')?.checked || false, price: document.getElementById('signal-price')?.checked || false, confidenceThreshold: parseInt(document.getElementById('signal-confidence')?.value) || 70, priceChangeThreshold: parseInt(document.getElementById('price-change-threshold')?.value) || 5, whaleThreshold: parseInt(document.getElementById('whale-threshold')?.value) || 100000, watchedCoins: document.getElementById('watched-coins')?.value || 'BTC, ETH, SOL', }; if (this.saveSettings()) { Toast.success('Signal settings saved'); this.sendSignalsToBackend(); } else { Toast.error('Failed to save signal settings'); } } async sendSignalsToBackend() { try { await api.post('/settings/signals', this.settings.signals); } catch (error) { console.warn('[Settings] Could not sync signal settings with backend:', error); } } saveScheduling() { this.settings.scheduling = { autoRefreshEnabled: document.getElementById('auto-refresh-enabled')?.checked || false, intervalMarket: parseInt(document.getElementById('interval-market')?.value) || 30, intervalNews: parseInt(document.getElementById('interval-news')?.value) || 120, intervalSentiment: parseInt(document.getElementById('interval-sentiment')?.value) || 300, intervalWhale: parseInt(document.getElementById('interval-whale')?.value) || 60, intervalBlockchain: parseInt(document.getElementById('interval-blockchain')?.value) || 300, intervalModels: parseInt(document.getElementById('interval-models')?.value) || 600, quietHoursEnabled: document.getElementById('quiet-hours-enabled')?.checked || false, quietStart: document.getElementById('quiet-start')?.value || '22:00', quietEnd: document.getElementById('quiet-end')?.value || '08:00', }; if (this.saveSettings()) { Toast.success('Schedule settings saved'); this.applyScheduling(); } else { Toast.error('Failed to save schedule settings'); } } applyScheduling() { // Dispatch custom event for other components to react window.dispatchEvent(new CustomEvent('settingsChanged', { detail: { scheduling: this.settings.scheduling } })); } saveNotifications() { this.settings.notifications = { browser: document.getElementById('notif-browser')?.checked || false, sound: document.getElementById('notif-sound')?.checked || false, toast: document.getElementById('notif-toast')?.checked || false, soundType: document.getElementById('notif-sound-type')?.value || 'default', volume: parseInt(document.getElementById('notif-volume')?.value) || 50, }; if (this.saveSettings()) { Toast.success('Notification settings saved'); } else { Toast.error('Failed to save notification settings'); } } testNotification() { // Test browser notification if (this.settings.notifications.browser && 'Notification' in window) { if (Notification.permission === 'granted') { new Notification('Crypto Monitor ULTIMATE', { body: 'Test notification! Your settings are working.', icon: '/static/assets/icons/favicon.svg' }); } else if (Notification.permission !== 'denied') { Notification.requestPermission().then(permission => { if (permission === 'granted') { new Notification('Crypto Monitor ULTIMATE', { body: 'Notifications enabled successfully!', icon: '/static/assets/icons/favicon.svg' }); } }); } } // Test toast if (this.settings.notifications.toast) { Toast.info('Test notification! Your settings are working.'); } // Test sound (placeholder - would need audio files) if (this.settings.notifications.sound) { console.log('[Settings] Would play sound:', this.settings.notifications.soundType); } } saveAppearance() { this.settings.appearance = { theme: document.querySelector('input[name="theme"]:checked')?.value || 'dark', compactMode: document.getElementById('compact-mode')?.checked || false, showAnimations: document.getElementById('show-animations')?.checked || true, showBgEffects: document.getElementById('show-bg-effects')?.checked || true, }; if (this.saveSettings()) { Toast.success('Appearance settings saved'); this.applySettings(); } else { Toast.error('Failed to save appearance settings'); } } applySettings() { this.applyTheme(); this.applyAnimations(this.settings.appearance.showAnimations); this.applyBgEffects(this.settings.appearance.showBgEffects); this.applyCompactMode(this.settings.appearance.compactMode); } applyTheme() { const theme = this.settings.appearance.theme; if (theme === 'system') { const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light'); } else { document.documentElement.setAttribute('data-theme', theme); } } applyAnimations(enabled) { document.body.classList.toggle('no-animations', !enabled); } applyBgEffects(enabled) { const bgEffects = document.querySelector('.background-effects'); if (bgEffects) { bgEffects.style.display = enabled ? 'block' : 'none'; } } applyCompactMode(enabled) { document.body.classList.toggle('compact-mode', enabled); } saveAllSettings() { this.saveTokens(); this.saveTelegram(); this.saveSignals(); this.saveScheduling(); this.saveNotifications(); this.saveAppearance(); Toast.success('All settings saved successfully!'); } resetSettings() { if (confirm('Are you sure you want to reset all settings to defaults? This cannot be undone.')) { this.settings = { ...DEFAULT_SETTINGS }; this.saveSettings(); this.populateForm(); this.applySettings(); Toast.info('Settings reset to defaults'); } } } // Initialize page const page = new SettingsPage(); window.settingsPage = page; // Export settings getter for other modules export function getSettings() { return page.settings; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => page.init()); } else { page.init(); }