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