| |
| |
| |
|
|
|
|
| class FeatureFlagsManager {
|
| constructor() {
|
| this.flags = {};
|
| this.localStorageKey = 'crypto_monitor_feature_flags';
|
| this.apiEndpoint = '/api/feature-flags';
|
| this.listeners = [];
|
| }
|
|
|
| |
| |
|
|
| async init() {
|
|
|
| this.loadFromLocalStorage();
|
|
|
|
|
| await this.syncWithBackend();
|
|
|
|
|
| setInterval(() => this.syncWithBackend(), 30000);
|
|
|
| return this.flags;
|
| }
|
|
|
| |
| |
|
|
| loadFromLocalStorage() {
|
| try {
|
| const stored = localStorage.getItem(this.localStorageKey);
|
| if (stored) {
|
| const data = JSON.parse(stored);
|
| this.flags = data.flags || {};
|
| console.log('[FeatureFlags] Loaded from localStorage:', this.flags);
|
| }
|
| } catch (error) {
|
| console.error('[FeatureFlags] Error loading from localStorage:', error);
|
| }
|
| }
|
|
|
| |
| |
|
|
| saveToLocalStorage() {
|
| try {
|
| const data = {
|
| flags: this.flags,
|
| updated_at: new Date().toISOString()
|
| };
|
| localStorage.setItem(this.localStorageKey, JSON.stringify(data));
|
| console.log('[FeatureFlags] Saved to localStorage');
|
| } catch (error) {
|
| console.error('[FeatureFlags] Error saving to localStorage:', error);
|
| }
|
| }
|
|
|
| |
| |
|
|
| async syncWithBackend() {
|
| try {
|
| const response = await fetch(this.apiEndpoint);
|
| if (!response.ok) {
|
| throw new Error(`HTTP ${response.status}`);
|
| }
|
|
|
| const data = await response.json();
|
| this.flags = data.flags || {};
|
| this.saveToLocalStorage();
|
| this.notifyListeners();
|
|
|
| console.log('[FeatureFlags] Synced with backend:', this.flags);
|
| return this.flags;
|
| } catch (error) {
|
| console.error('[FeatureFlags] Error syncing with backend:', error);
|
|
|
| return this.flags;
|
| }
|
| }
|
|
|
| |
| |
|
|
| isEnabled(flagName) {
|
| return this.flags[flagName] === true;
|
| }
|
|
|
| |
| |
|
|
| getAll() {
|
| return { ...this.flags };
|
| }
|
|
|
| |
| |
|
|
| async setFlag(flagName, value) {
|
| try {
|
| const response = await fetch(`${this.apiEndpoint}/${flagName}`, {
|
| method: 'PUT',
|
| headers: {
|
| 'Content-Type': 'application/json',
|
| },
|
| body: JSON.stringify({
|
| flag_name: flagName,
|
| value: value
|
| })
|
| });
|
|
|
| if (!response.ok) {
|
| throw new Error(`HTTP ${response.status}`);
|
| }
|
|
|
| const data = await response.json();
|
| if (data.success) {
|
| this.flags[flagName] = value;
|
| this.saveToLocalStorage();
|
| this.notifyListeners();
|
| console.log(`[FeatureFlags] Set ${flagName} = ${value}`);
|
| return true;
|
| }
|
|
|
| return false;
|
| } catch (error) {
|
| console.error(`[FeatureFlags] Error setting flag ${flagName}:`, error);
|
| return false;
|
| }
|
| }
|
|
|
| |
| |
|
|
| async updateFlags(updates) {
|
| try {
|
| const response = await fetch(this.apiEndpoint, {
|
| method: 'PUT',
|
| headers: {
|
| 'Content-Type': 'application/json',
|
| },
|
| body: JSON.stringify({
|
| flags: updates
|
| })
|
| });
|
|
|
| if (!response.ok) {
|
| throw new Error(`HTTP ${response.status}`);
|
| }
|
|
|
| const data = await response.json();
|
| if (data.success) {
|
| this.flags = data.flags;
|
| this.saveToLocalStorage();
|
| this.notifyListeners();
|
| console.log('[FeatureFlags] Updated flags:', updates);
|
| return true;
|
| }
|
|
|
| return false;
|
| } catch (error) {
|
| console.error('[FeatureFlags] Error updating flags:', error);
|
| return false;
|
| }
|
| }
|
|
|
| |
| |
|
|
| async resetToDefaults() {
|
| try {
|
| const response = await fetch(`${this.apiEndpoint}/reset`, {
|
| method: 'POST'
|
| });
|
|
|
| if (!response.ok) {
|
| throw new Error(`HTTP ${response.status}`);
|
| }
|
|
|
| const data = await response.json();
|
| if (data.success) {
|
| this.flags = data.flags;
|
| this.saveToLocalStorage();
|
| this.notifyListeners();
|
| console.log('[FeatureFlags] Reset to defaults');
|
| return true;
|
| }
|
|
|
| return false;
|
| } catch (error) {
|
| console.error('[FeatureFlags] Error resetting flags:', error);
|
| return false;
|
| }
|
| }
|
|
|
| |
| |
|
|
| onChange(callback) {
|
| this.listeners.push(callback);
|
| return () => {
|
| const index = this.listeners.indexOf(callback);
|
| if (index > -1) {
|
| this.listeners.splice(index, 1);
|
| }
|
| };
|
| }
|
|
|
| |
| |
|
|
| notifyListeners() {
|
| this.listeners.forEach(callback => {
|
| try {
|
| callback(this.flags);
|
| } catch (error) {
|
| console.error('[FeatureFlags] Error in listener:', error);
|
| }
|
| });
|
| }
|
|
|
| |
| |
|
|
| renderUI(containerId) {
|
| const container = document.getElementById(containerId);
|
| if (!container) {
|
| console.error(`[FeatureFlags] Container #${containerId} not found`);
|
| return;
|
| }
|
|
|
| const flagDescriptions = {
|
| enableWhaleTracking: 'Show whale transaction tracking',
|
| enableMarketOverview: 'Display market overview dashboard',
|
| enableFearGreedIndex: 'Show Fear & Greed sentiment index',
|
| enableNewsFeed: 'Display cryptocurrency news feed',
|
| enableSentimentAnalysis: 'Enable sentiment analysis features',
|
| enableMlPredictions: 'Show ML-powered price predictions',
|
| enableProxyAutoMode: 'Automatic proxy for failing APIs',
|
| enableDefiProtocols: 'Display DeFi protocol data',
|
| enableTrendingCoins: 'Show trending cryptocurrencies',
|
| enableGlobalStats: 'Display global market statistics',
|
| enableProviderRotation: 'Enable provider rotation system',
|
| enableWebSocketStreaming: 'Real-time WebSocket updates',
|
| enableDatabaseLogging: 'Log provider health to database',
|
| enableRealTimeAlerts: 'Show real-time alert notifications',
|
| enableAdvancedCharts: 'Display advanced charting',
|
| enableExportFeatures: 'Enable data export functions',
|
| enableCustomProviders: 'Allow custom API providers',
|
| enablePoolManagement: 'Enable provider pool management',
|
| enableHFIntegration: 'HuggingFace model integration'
|
| };
|
|
|
| let html = '<div class="feature-flags-container">';
|
| html += '<h3>Feature Flags</h3>';
|
| html += '<div class="feature-flags-list">';
|
|
|
| Object.keys(this.flags).forEach(flagName => {
|
| const enabled = this.flags[flagName];
|
| const description = flagDescriptions[flagName] || flagName;
|
|
|
| html += `
|
| <div class="feature-flag-item">
|
| <label class="feature-flag-label">
|
| <input
|
| type="checkbox"
|
| class="feature-flag-toggle"
|
| data-flag="${flagName}"
|
| ${enabled ? 'checked' : ''}
|
| />
|
| <span class="feature-flag-name">${description}</span>
|
| </label>
|
| <span class="feature-flag-status ${enabled ? 'enabled' : 'disabled'}">
|
| ${enabled ? '✓ Enabled' : '✗ Disabled'}
|
| </span>
|
| </div>
|
| `;
|
| });
|
|
|
| html += '</div>';
|
| html += '<div class="feature-flags-actions">';
|
| html += '<button id="ff-reset-btn" class="btn btn-secondary">Reset to Defaults</button>';
|
| html += '</div>';
|
| html += '</div>';
|
|
|
| container.innerHTML = html;
|
|
|
|
|
| container.querySelectorAll('.feature-flag-toggle').forEach(toggle => {
|
| toggle.addEventListener('change', async (e) => {
|
| const flagName = e.target.dataset.flag;
|
| const value = e.target.checked;
|
| await this.setFlag(flagName, value);
|
| });
|
| });
|
|
|
| const resetBtn = container.querySelector('#ff-reset-btn');
|
| if (resetBtn) {
|
| resetBtn.addEventListener('click', async () => {
|
| if (confirm('Reset all feature flags to defaults?')) {
|
| await this.resetToDefaults();
|
| this.renderUI(containerId);
|
| }
|
| });
|
| }
|
|
|
|
|
| this.onChange(() => {
|
| this.renderUI(containerId);
|
| });
|
| }
|
| }
|
|
|
|
|
| window.featureFlagsManager = new FeatureFlagsManager();
|
|
|
|
|
| document.addEventListener('DOMContentLoaded', () => {
|
| window.featureFlagsManager.init().then(() => {
|
| console.log('[FeatureFlags] Initialized');
|
| });
|
| });
|
|
|