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