diff --git a/app/__init__.py b/app/__init__.py index 36f592b..24d2037 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -5,19 +5,25 @@ Creates and configures the Flask application with SocketIO support for real-time updates of the Markov economics simulation. """ +import os from flask import Flask from flask_socketio import SocketIO from config import config from app.models import SimulationManager -# Initialize SocketIO -socketio = SocketIO(cors_allowed_origins="*") +# Initialize SocketIO with enhanced proxy support +socketio = SocketIO( + cors_allowed_origins="*", + async_mode='threading', + logger=True, + engineio_logger=True +) # Global simulation manager instance simulation_manager = SimulationManager() -def create_app(config_name='development'): +def create_app(config_name=None): """ Create and configure the Flask application. @@ -27,11 +33,20 @@ def create_app(config_name='development'): Returns: Configured Flask application instance """ + if config_name is None: + config_name = os.getenv('FLASK_CONFIG', os.getenv('FLASK_ENV', 'development')) + app = Flask(__name__) app.config.from_object(config[config_name]) - # Initialize extensions - socketio.init_app(app, async_mode='threading') + # Initialize extensions with proxy-friendly settings + socketio.init_app( + app, + async_mode='threading', + cors_allowed_origins="*", + allow_upgrades=True, + transports=['websocket', 'polling'] + ) # Register blueprints from .routes.main import main_bp diff --git a/app/static/js/simulation.js b/app/static/js/simulation.js index f39c0b4..c90f52e 100644 --- a/app/static/js/simulation.js +++ b/app/static/js/simulation.js @@ -1,3 +1,19 @@ +// Debug flag - can be enabled even in production for troubleshooting +const DEBUG_DISTRIBUTION = true; + +/** + * Debug logger that works in production + */ +function debugLog(message, data = null) { + if (DEBUG_DISTRIBUTION) { + if (data) { + console.log(`[DISTRIBUTION DEBUG] ${message}:`, data); + } else { + console.log(`[DISTRIBUTION DEBUG] ${message}`); + } + } +} + /** * Simulation Control and Visualization * @@ -252,55 +268,60 @@ function initializeCharts() { // Wealth Distribution Chart const distributionCtx = document.getElementById('distributionChart'); - console.log('Initializing distribution chart:', { + debugLog('Initializing distribution chart', { hasCanvas: !!distributionCtx, - canvasId: distributionCtx ? distributionCtx.id : 'not found' + canvasId: distributionCtx ? distributionCtx.id : 'not found', + chartJsAvailable: typeof Chart !== 'undefined' }); if (distributionCtx) { - charts.distribution = new Chart(distributionCtx, { - type: 'bar', - data: { - labels: [], - datasets: [{ - label: 'Number of Agents', - data: [], - backgroundColor: 'rgba(40, 167, 69, 0.7)', - borderColor: '#28a745', - borderWidth: 1 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - title: { - display: true, - text: 'Wealth Distribution' - }, - legend: { - display: false - } + try { + charts.distribution = new Chart(distributionCtx, { + type: 'bar', + data: { + labels: [], + datasets: [{ + label: 'Number of Agents', + data: [], + backgroundColor: 'rgba(40, 167, 69, 0.7)', + borderColor: '#28a745', + borderWidth: 1 + }] }, - scales: { - x: { + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { title: { display: true, - text: 'Wealth Range' + text: 'Wealth Distribution' + }, + legend: { + display: false } }, - y: { - title: { - display: true, - text: 'Number of Agents' + scales: { + x: { + title: { + display: true, + text: 'Wealth Range' + } + }, + y: { + title: { + display: true, + text: 'Number of Agents' + } } } } - } - }); - console.log('✅ Distribution chart initialized successfully'); + }); + debugLog('✅ Distribution chart initialized successfully'); + } catch (error) { + debugLog('❌ Error initializing distribution chart', error); + } } else { - console.error('❌ Distribution chart canvas not found!'); + debugLog('❌ Distribution chart canvas not found!'); } } @@ -339,23 +360,53 @@ function initializeEventListeners() { } /** - * Initialize real-time updates via Socket.IO + * Initialize real-time updates via Socket.IO with fallback polling */ function initializeRealtimeUpdates() { if (!window.MarkovEconomics.socket) { - console.warn('Socket.IO not available - real-time updates disabled'); + debugLog('Socket.IO not available - real-time updates disabled'); return; } const socket = window.MarkovEconomics.socket; + // Monitor connection status + socket.on('connect', function() { + debugLog('Socket.IO connected successfully'); + window.MarkovEconomics.isConnected = true; + }); + + socket.on('disconnect', function() { + debugLog('Socket.IO disconnected'); + window.MarkovEconomics.isConnected = false; + + // Start fallback polling if simulation is running + if (currentSimulation.isRunning) { + debugLog('Starting fallback polling due to disconnection'); + startFallbackPolling(); + } + }); + + socket.on('connect_error', function(error) { + debugLog('Socket.IO connection error', error); + window.MarkovEconomics.isConnected = false; + + // Start fallback polling + if (currentSimulation.isRunning) { + debugLog('Starting fallback polling due to connection error'); + startFallbackPolling(); + } + }); + // Simulation progress updates socket.on('simulation_progress', function(data) { + debugLog('Received real-time progress update'); updateSimulationProgress(data); }); // Simulation completion socket.on('simulation_complete', function(data) { + debugLog('Received simulation complete event'); onSimulationComplete(data); }); @@ -370,6 +421,61 @@ function initializeRealtimeUpdates() { }); } +/** + * Fallback polling mechanism when WebSocket fails + */ +let fallbackPollingInterval = null; + +function startFallbackPolling() { + if (fallbackPollingInterval) { + clearInterval(fallbackPollingInterval); + } + + debugLog('Starting fallback polling mechanism'); + fallbackPollingInterval = setInterval(async () => { + if (!currentSimulation.isRunning || !currentSimulation.id) { + stopFallbackPolling(); + return; + } + + try { + const response = await window.MarkovEconomics.utils.apiRequest( + `/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true` + ); + + if (response.latest_snapshot) { + // Simulate progress update + updateSimulationProgress({ + iteration: response.latest_snapshot.iteration, + total_wealth: response.latest_snapshot.total_wealth, + gini_coefficient: response.latest_snapshot.gini_coefficient, + capital_share: response.latest_snapshot.capital_share, + wealth_concentration_top10: response.latest_snapshot.wealth_concentration_top10, + distribution: response.distribution, + progress_percentage: (response.latest_snapshot.iteration / currentSimulation.parameters.iterations) * 100 + }); + } + + // Check if simulation is complete + if (response.latest_snapshot && response.latest_snapshot.iteration >= currentSimulation.parameters.iterations) { + debugLog('Simulation completed via fallback polling'); + stopFallbackPolling(); + onSimulationComplete({ total_snapshots: response.latest_snapshot.iteration }); + } + } catch (error) { + debugLog('Fallback polling error', error); + } + }, 1000); // Poll every second +} + +function stopFallbackPolling() { + if (fallbackPollingInterval) { + debugLog('Stopping fallback polling'); + clearInterval(fallbackPollingInterval); + fallbackPollingInterval = null; + } +} + /** * Start a new simulation */ @@ -400,11 +506,19 @@ async function startSimulation() { currentSimulation.id = createResponse.simulation_id; currentSimulation.parameters = parameters; + debugLog('Simulation created', { + id: currentSimulation.id, + parameters: currentSimulation.parameters + }); + // Join simulation room for real-time updates - if (window.MarkovEconomics.socket) { + if (window.MarkovEconomics.socket && window.MarkovEconomics.isConnected) { + debugLog('Joining simulation room via WebSocket'); window.MarkovEconomics.socket.emit('join_simulation', { simulation_id: currentSimulation.id }); + } else { + debugLog('WebSocket not available, will use fallback polling'); } // Start simulation @@ -415,6 +529,12 @@ async function startSimulation() { currentSimulation.isRunning = true; updateUIState('running'); + // Start fallback polling if WebSocket is not connected + if (!window.MarkovEconomics.socket || !window.MarkovEconomics.isConnected) { + debugLog('Starting fallback polling for simulation progress'); + startFallbackPolling(); + } + window.MarkovEconomics.utils.showNotification('Simulation started successfully!', 'success'); } catch (error) { @@ -437,6 +557,9 @@ async function stopSimulation() { currentSimulation.isRunning = false; updateUIState('stopped'); + // Stop fallback polling if active + stopFallbackPolling(); + window.MarkovEconomics.utils.showNotification('Simulation stopped', 'info'); } catch (error) { @@ -613,13 +736,14 @@ function updateSimulationProgress(data) { currentSimulation.data.top10Share.push(data.wealth_concentration_top10 || 0); // Update distribution data if available - console.log('Received simulation progress data:', { + debugLog('Received simulation progress data', { hasDistribution: !!data.distribution, - distribution: data.distribution + distribution: data.distribution, + socketConnected: window.MarkovEconomics ? window.MarkovEconomics.isConnected : 'unknown' }); if (data.distribution && data.distribution.labels && data.distribution.counts) { - console.log('Updating distribution data:', { + debugLog('Updating distribution data', { labelsLength: data.distribution.labels.length, countsLength: data.distribution.counts.length, labels: data.distribution.labels, @@ -629,7 +753,7 @@ function updateSimulationProgress(data) { currentSimulation.data.distribution.labels = [...data.distribution.labels]; currentSimulation.data.distribution.counts = [...data.distribution.counts]; } else { - console.log('No valid distribution data in progress update'); + debugLog('No valid distribution data in progress update'); } updateCharts(); @@ -659,26 +783,35 @@ function updateCharts() { // Wealth Distribution Chart if (charts.distribution) { const distData = currentSimulation.data.distribution; - console.log('Updating distribution chart with data:', { + debugLog('Updating distribution chart with data', { hasLabels: !!(distData.labels && distData.labels.length > 0), hasCounts: !!(distData.counts && distData.counts.length > 0), labelsLength: distData.labels ? distData.labels.length : 0, countsLength: distData.counts ? distData.counts.length : 0, labels: distData.labels, - counts: distData.counts + counts: distData.counts, + chartExists: !!charts.distribution, + canvasElement: document.getElementById('distributionChart') }); if (distData.labels && distData.labels.length > 0 && distData.counts && distData.counts.length > 0) { - charts.distribution.data.labels = distData.labels; - charts.distribution.data.datasets[0].data = distData.counts; - charts.distribution.update('none'); - console.log('✅ Distribution chart updated successfully'); + try { + charts.distribution.data.labels = distData.labels; + charts.distribution.data.datasets[0].data = distData.counts; + charts.distribution.update('none'); + debugLog('✅ Distribution chart updated successfully'); + } catch (error) { + debugLog('❌ Error updating distribution chart', error); + } } else { - console.log('❌ Distribution chart not updated - insufficient data'); + debugLog('❌ Distribution chart not updated - insufficient data'); } } else { - console.log('❌ Distribution chart not found'); + debugLog('❌ Distribution chart not found', { + chartsObject: charts, + distributionElement: document.getElementById('distributionChart') + }); } } @@ -718,12 +851,12 @@ async function onSimulationComplete(data) { // Fetch complete simulation data and populate charts try { - console.log('Fetching complete simulation data...'); + debugLog('Fetching complete simulation data...'); const response = await window.MarkovEconomics.utils.apiRequest( `/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true` ); - console.log('Complete simulation data received:', { + debugLog('Complete simulation data received', { hasEvolution: !!response.evolution, hasDistribution: !!response.distribution, distribution: response.distribution @@ -739,7 +872,7 @@ async function onSimulationComplete(data) { // Update distribution data if (response.distribution && response.distribution.labels && response.distribution.counts) { - console.log('Setting final distribution data:', { + debugLog('Setting final distribution data', { labelsLength: response.distribution.labels.length, countsLength: response.distribution.counts.length, labels: response.distribution.labels, @@ -749,19 +882,20 @@ async function onSimulationComplete(data) { currentSimulation.data.distribution.labels = [...response.distribution.labels]; currentSimulation.data.distribution.counts = [...response.distribution.counts]; } else { - console.log('No distribution data in final response'); + debugLog('No distribution data in final response'); // Fallback: try to get distribution data from dedicated endpoint try { + debugLog('Attempting fallback distribution fetch...'); const distResponse = await window.MarkovEconomics.utils.apiRequest( `/api/simulation/${currentSimulation.id}/distribution?bins=10` ); if (distResponse.histogram && distResponse.histogram.labels && distResponse.histogram.counts) { - console.log('Got distribution data from dedicated endpoint:', distResponse.histogram); + debugLog('Got distribution data from dedicated endpoint', distResponse.histogram); currentSimulation.data.distribution.labels = [...distResponse.histogram.labels]; currentSimulation.data.distribution.counts = [...distResponse.histogram.counts]; } } catch (distError) { - console.error('Failed to fetch distribution data from dedicated endpoint:', distError); + debugLog('Failed to fetch distribution data from dedicated endpoint', distError); } } diff --git a/run.py b/run.py index d9e06d5..9383364 100644 --- a/run.py +++ b/run.py @@ -7,14 +7,15 @@ Demonstrates how capitalism "eats the world" using Markov chains. import os from app import create_app, socketio -config_name = os.getenv('FLASK_CONFIG', 'development') +# Use FLASK_ENV if set, otherwise default to development +config_name = os.getenv('FLASK_ENV', 'development') app = create_app(config_name) if __name__ == '__main__': - debug_mode = os.getenv('FLASK_ENV', 'development') == 'development' + debug_mode = config_name == 'development' # For production deployment, allow unsafe werkzeug or use a proper WSGI server - if os.getenv('FLASK_ENV') == 'production': + if config_name == 'production': socketio.run(app, debug=False, host='0.0.0.0', port=5000, allow_unsafe_werkzeug=True) else: socketio.run(app, debug=debug_mode, host='0.0.0.0', port=5000) \ No newline at end of file