Compare commits

...

8 Commits

12 changed files with 368 additions and 96 deletions

View File

@@ -14,13 +14,13 @@ This directory contains Docker configuration files for containerizing the Markov
```bash
# Build and start the application
docker-compose up --build
docker compose up --build
# Run in detached mode
docker-compose up -d --build
docker compose up -d --build
# Stop the application
docker-compose down
docker compose down
```
### Using Docker directly

View File

@@ -36,5 +36,5 @@ EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:5000/health', timeout=2)" || exit 1
# Run the application with Gunicorn using gevent workers for SocketIO compatibility
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--worker-class", "gevent", "--worker-connections", "1000", "--timeout", "30", "wsgi:app"]
# Run the application with Flask's built-in server
CMD ["python", "run.py"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Capitalism Eats The World
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -25,7 +25,7 @@ This project provides an interactive simulation of economic dynamics, particular
1. Clone the repository:
```bash
git clone https://github.com/[user]/capitalism-eats-the-world.git
git clone https://gitea.elpatron.me/elpatron/capitalism-eats-the-world.git
cd capitalism-eats-the-world
```
@@ -40,14 +40,14 @@ This project provides an interactive simulation of economic dynamics, particular
For production deployment with automatic SSL certificates, the project includes a Caddy configuration:
1. Update the [Caddyfile](Caddyfile) with your email address for Let's Encrypt notifications
1. Update the [Caddyfile](Caddyfile) with your email address and domain for Let's Encrypt notifications
2. Ensure your domain points to your server's IP address
3. Run with Caddy:
```bash
docker-compose up -d
```
The application will be available at `https://markov.elpatron.me` with automatic SSL certificate management.
The application will be available at `https://markov.example.com` with automatic SSL certificate management.
## Documentation

View File

@@ -318,9 +318,6 @@ class EconomicSimulation:
if not wealth_values:
return [], []
# Debug logging
print(f"DEBUG: Wealth values - count: {len(wealth_values)}, min: {min(wealth_values)}, max: {max(wealth_values)}")
# Calculate histogram bins
min_wealth = min(wealth_values)
max_wealth = max(wealth_values)
@@ -328,7 +325,6 @@ class EconomicSimulation:
if min_wealth == max_wealth:
# All agents have same wealth
result = [f"${min_wealth:.0f}"], [len(wealth_values)]
print(f"DEBUG: All agents have same wealth - labels: {result[0]}, counts: {result[1]}")
return result
# Create bins
@@ -357,7 +353,6 @@ class EconomicSimulation:
if bin_start <= wealth < bin_end:
bin_counts[i] += 1
print(f"DEBUG: Histogram result - labels: {bin_labels}, counts: {bin_counts}")
return bin_labels, bin_counts
def update_parameters(self, new_parameters: SimulationParameters):

View File

@@ -139,19 +139,29 @@ def start_simulation(simulation_id: str):
try:
# Run simulation with progress updates
total_iterations = simulation.parameters.iterations
last_update_time = time.time()
update_interval = max(0.1, min(1.0, 1000 / total_iterations)) # Dynamic update interval
for i in range(total_iterations):
if not simulation.is_running:
break
snapshot = simulation.step()
# Emit progress update every 10 iterations or at milestones
if i % 10 == 0 or i == total_iterations - 1:
# Get distribution data for real-time chart updates
bin_labels, bin_counts = simulation.get_wealth_histogram(10)
# Emit progress update at dynamic intervals for better performance
current_time = time.time()
if (i % max(1, total_iterations // 100) == 0 or # Every 1% of iterations
i == total_iterations - 1 or # Last iteration
current_time - last_update_time >= update_interval): # Time-based update
# Debug logging
print(f"DEBUG: Sending distribution data - labels: {bin_labels}, counts: {bin_counts}")
# Get distribution data for real-time chart updates (but less frequently)
distribution_data = None
if i % max(1, total_iterations // 20) == 0 or i == total_iterations - 1:
bin_labels, bin_counts = simulation.get_wealth_histogram(8) # Reduced bins for performance
distribution_data = {
'labels': bin_labels,
'counts': bin_counts
}
progress_data = {
'simulation_id': simulation_id,
@@ -161,18 +171,19 @@ def start_simulation(simulation_id: str):
'total_wealth': snapshot.total_wealth,
'gini_coefficient': snapshot.gini_coefficient,
'capital_share': snapshot.capital_share,
'wealth_concentration_top10': snapshot.wealth_concentration_top10,
'distribution': {
'labels': bin_labels,
'counts': bin_counts
}
'wealth_concentration_top10': snapshot.wealth_concentration_top10
}
# Only include distribution data when we have it
if distribution_data:
progress_data['distribution'] = distribution_data
socketio.emit('simulation_progress', progress_data,
room=f'simulation_{simulation_id}')
last_update_time = current_time
# Small delay to allow real-time visualization
time.sleep(0.01)
# Adaptive delay based on performance
time.sleep(max(0.001, 0.01 * (total_iterations / 1000)))
# Mark as completed
simulation.is_running = False
@@ -363,11 +374,21 @@ def get_wealth_distribution(simulation_id: str):
if num_bins < 1 or num_bins > 50:
num_bins = 10 # Default to 10 bins
# Optimize bin count based on agent count for better performance
agent_count = len(simulation.agents) if simulation.agents else 0
if agent_count > 0:
# Reduce bin count for small agent populations to improve performance
if agent_count < 50 and num_bins > agent_count // 2:
num_bins = max(3, agent_count // 2)
# Cap bin count for very large simulations to prevent performance issues
elif agent_count > 1000 and num_bins > 25:
num_bins = 25
# Get histogram data
bin_labels, bin_counts = simulation.get_wealth_histogram(num_bins)
# Debug logging
print(f"DEBUG: Distribution endpoint - labels: {bin_labels}, counts: {bin_counts}, bins: {num_bins}")
# print(f"DEBUG: Distribution endpoint - labels: {bin_labels}, counts: {bin_counts}, bins: {num_bins}")
response_data = {
'simulation_id': simulation_id,
@@ -379,7 +400,7 @@ def get_wealth_distribution(simulation_id: str):
}
}
print(f"DEBUG: Distribution endpoint response: {response_data}")
# print(f"DEBUG: Distribution endpoint response: {response_data}")
return jsonify(response_data)
except Exception as e:

View File

@@ -191,10 +191,14 @@ def test_simulation():
def debug_info():
"""
Debug endpoint to verify deployment and test functionality.
Returns:
JSON response with debug information
Only available in development mode.
"""
from flask import current_app
# Only allow debug endpoint in development mode
if not current_app.config.get('DEVELOPMENT', False):
return jsonify({'error': 'Debug endpoint not available in production'}), 403
import os
from datetime import datetime
from app.models.economic_model import EconomicSimulation, SimulationParameters

View File

@@ -26,13 +26,13 @@ function initializeSocket() {
window.MarkovEconomics.socket = io();
window.MarkovEconomics.socket.on('connect', function() {
console.log('Connected to server');
// console.log('Connected to server');
window.MarkovEconomics.isConnected = true;
updateConnectionStatus(true);
});
window.MarkovEconomics.socket.on('disconnect', function() {
console.log('Disconnected from server');
// console.log('Disconnected from server');
window.MarkovEconomics.isConnected = false;
updateConnectionStatus(false);
});
@@ -50,9 +50,9 @@ function initializeSocket() {
function updateConnectionStatus(connected) {
// You can add a connection status indicator here if needed
if (connected) {
console.log('✅ Real-time connection established');
// console.log('✅ Real-time connection established');
} else {
console.log('❌ Real-time connection lost');
// console.log('❌ Real-time connection lost');
}
}
@@ -251,7 +251,7 @@ document.addEventListener('DOMContentLoaded', function() {
card.classList.add('fade-in');
});
console.log('🚀 Markov Economics application initialized');
// console.log('🚀 Markov Economics application initialized');
});
/**
@@ -259,9 +259,9 @@ document.addEventListener('DOMContentLoaded', function() {
*/
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
console.log('Page hidden - pausing updates');
// console.log('Page hidden - pausing updates');
} else {
console.log('Page visible - resuming updates');
// console.log('Page visible - resuming updates');
}
});

View File

@@ -256,7 +256,7 @@ window.testDistributionChart = function() {
};
// Debug flag - can be enabled even in production for troubleshooting
const DEBUG_DISTRIBUTION = true;
const DEBUG_DISTRIBUTION = false;
/**
* Debug logger that works in production
@@ -676,7 +676,7 @@ function initializeRealtimeUpdates() {
// Simulation progress updates
socket.on('simulation_progress', function(data) {
debugLog('Received real-time progress update');
updateSimulationProgress(data);
onSimulationProgress(data);
});
// Simulation completion
@@ -714,13 +714,14 @@ function startFallbackPolling() {
}
try {
const response = await window.MarkovEconomics.utils.apiRequest(
// Use enhanced API request with batching support
const response = await enhancedApiRequest(
`/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true`
);
if (response.latest_snapshot) {
// Simulate progress update
updateSimulationProgress({
onSimulationProgress({
iteration: response.latest_snapshot.iteration,
total_wealth: response.latest_snapshot.total_wealth,
gini_coefficient: response.latest_snapshot.gini_coefficient,
@@ -772,8 +773,8 @@ async function startSimulation() {
// Update UI state
updateUIState('starting');
// Create simulation
const createResponse = await window.MarkovEconomics.utils.apiRequest('/api/simulation', {
// Create simulation using enhanced API request
const createResponse = await enhancedApiRequest('/api/simulation', {
method: 'POST',
body: JSON.stringify(parameters)
});
@@ -796,8 +797,8 @@ async function startSimulation() {
debugLog('WebSocket not available, will use fallback polling');
}
// Start simulation
await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/start`, {
// Start simulation using enhanced API request
await enhancedApiRequest(`/api/simulation/${currentSimulation.id}/start`, {
method: 'POST'
});
@@ -825,7 +826,8 @@ async function stopSimulation() {
if (!currentSimulation.id) return;
try {
await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/stop`, {
// Use enhanced API request with batching support
await enhancedApiRequest(`/api/simulation/${currentSimulation.id}/stop`, {
method: 'POST'
});
@@ -989,76 +991,173 @@ function updateUIState(state) {
}
/**
* Update simulation progress
* Calculate adaptive update interval based on simulation parameters
* Reduces update frequency for larger simulations to improve performance
*/
function updateSimulationProgress(data) {
// Update progress bar
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
function getAdaptiveUpdateInterval() {
if (!currentSimulation.parameters.num_agents || !currentSimulation.parameters.iterations) {
return 50; // Default to 50ms
}
if (progressBar && progressText) {
const percentage = data.progress_percentage || 0;
const agentCount = currentSimulation.parameters.num_agents;
const iterationCount = currentSimulation.parameters.iterations;
// Base interval (ms)
let interval = 50;
// Increase interval for larger simulations
if (agentCount > 1000) {
interval = Math.min(200, 50 + (agentCount / 100));
} else if (agentCount > 500) {
interval = 100;
}
// Further adjust based on iteration count
if (iterationCount > 50000) {
interval *= 2;
} else if (iterationCount > 10000) {
interval *= 1.5;
}
// Cap at reasonable values
return Math.min(500, Math.max(20, interval));
}
/**
* Handle simulation progress updates
*/
function onSimulationProgress(data) {
// Update progress bar
const progressBar = document.getElementById('simulationProgressBar');
const progressText = document.getElementById('progressText');
if (progressBar && progressText && data.progress_percentage !== undefined) {
const percentage = Math.min(100, Math.max(0, data.progress_percentage));
progressBar.style.width = percentage + '%';
progressText.textContent = percentage.toFixed(1) + '%';
}
// Update charts and metrics
// Update charts and metrics only if we have new data
if (data.iteration !== undefined) {
currentSimulation.data.iterations.push(data.iteration);
currentSimulation.data.totalWealth.push(data.total_wealth || 0);
currentSimulation.data.giniCoefficients.push(data.gini_coefficient || 0);
currentSimulation.data.capitalShare.push(data.capital_share || 0);
currentSimulation.data.top10Share.push(data.wealth_concentration_top10 || 0);
// Only update time series data occasionally to improve performance
const shouldUpdateSeries = currentSimulation.data.iterations.length === 0 ||
data.iteration % Math.max(1, Math.floor(currentSimulation.parameters.iterations / 200)) === 0 ||
data.iteration === (currentSimulation.parameters.iterations - 1);
if (shouldUpdateSeries) {
currentSimulation.data.iterations.push(data.iteration);
currentSimulation.data.totalWealth.push(data.total_wealth || 0);
currentSimulation.data.giniCoefficients.push(data.gini_coefficient || 0);
currentSimulation.data.capitalShare.push(data.capital_share || 0);
currentSimulation.data.top10Share.push(data.wealth_concentration_top10 || 0);
}
// Update distribution data if available
debugLog('Received simulation progress data', {
hasDistribution: !!data.distribution,
distribution: data.distribution,
socketConnected: window.MarkovEconomics ? window.MarkovEconomics.isConnected : 'unknown'
});
// More robust handling of distribution data
if (data.distribution &&
Array.isArray(data.distribution.labels) &&
Array.isArray(data.distribution.counts) &&
data.distribution.labels.length > 0 &&
data.distribution.counts.length > 0) {
debugLog('Updating distribution data', {
labelsLength: data.distribution.labels.length,
countsLength: data.distribution.counts.length,
labels: data.distribution.labels,
counts: data.distribution.counts
});
// Store the distribution data properly
currentSimulation.data.distribution.labels = [...data.distribution.labels];
currentSimulation.data.distribution.counts = [...data.distribution.counts];
} else {
debugLog('No valid distribution data in progress update');
} else if (data.iteration % Math.max(1, Math.floor(currentSimulation.parameters.iterations / 50)) === 0) {
// Periodically fetch distribution data if not provided in the update
// But less frequently for larger simulations
fetchDistributionData().then(histogram => {
if (histogram && histogram.labels && histogram.counts) {
currentSimulation.data.distribution.labels = [...histogram.labels];
currentSimulation.data.distribution.counts = [...histogram.counts];
}
});
}
// Adaptive throttling based on simulation size
const adaptiveInterval = getAdaptiveUpdateInterval();
if (!window.lastChartUpdate || (Date.now() - window.lastChartUpdate) > adaptiveInterval) {
updateCharts();
window.lastChartUpdate = Date.now();
}
updateCharts();
updateMetricsDisplay(data);
}
}
/**
* Update charts with new data
* Sample data for chart rendering to improve performance with large datasets
* @param {Array} data - Array of data points
* @param {number} maxPoints - Maximum number of points to display
* @returns {Array} - Sampled data
*/
function sampleDataForChart(data, maxPoints = 200) {
if (!Array.isArray(data) || data.length <= maxPoints) {
return data;
}
const sampled = [];
const step = Math.ceil(data.length / maxPoints);
for (let i = 0; i < data.length; i += step) {
sampled.push(data[i]);
}
return sampled;
}
/**
* Sample time series data for chart rendering
* @param {Object} chartData - Object containing time series data arrays
* @param {number} maxPoints - Maximum number of points to display
* @returns {Object} - Object with sampled data arrays
*/
function sampleTimeSeriesData(chartData, maxPoints = 200) {
if (!chartData.iterations || chartData.iterations.length <= maxPoints) {
return chartData;
}
const step = Math.ceil(chartData.iterations.length / maxPoints);
return {
iterations: sampleDataForChart(chartData.iterations, maxPoints),
totalWealth: sampleDataForChart(chartData.totalWealth, maxPoints),
giniCoefficients: sampleDataForChart(chartData.giniCoefficients, maxPoints),
capitalShare: sampleDataForChart(chartData.capitalShare, maxPoints),
top10Share: sampleDataForChart(chartData.top10Share, maxPoints)
};
}
/**
* Update charts with new data using sampling optimization
*/
function updateCharts() {
// Determine maximum points based on agent count for better performance
const maxChartPoints = currentSimulation.parameters.num_agents > 1000 ? 100 : 200;
// Wealth Evolution Chart
if (charts.wealthEvolution) {
charts.wealthEvolution.data.labels = currentSimulation.data.iterations;
charts.wealthEvolution.data.datasets[0].data = currentSimulation.data.totalWealth;
// Sample data for large datasets
const sampledData = sampleTimeSeriesData({
iterations: currentSimulation.data.iterations,
totalWealth: currentSimulation.data.totalWealth
}, maxChartPoints);
charts.wealthEvolution.data.labels = sampledData.iterations;
charts.wealthEvolution.data.datasets[0].data = sampledData.totalWealth;
charts.wealthEvolution.update('none');
}
// Inequality Chart
if (charts.inequality) {
charts.inequality.data.labels = currentSimulation.data.iterations;
charts.inequality.data.datasets[0].data = currentSimulation.data.giniCoefficients;
charts.inequality.data.datasets[1].data = currentSimulation.data.top10Share;
// Sample data for large datasets
const sampledData = sampleTimeSeriesData({
iterations: currentSimulation.data.iterations,
giniCoefficients: currentSimulation.data.giniCoefficients,
top10Share: currentSimulation.data.top10Share
}, maxChartPoints);
charts.inequality.data.labels = sampledData.iterations;
charts.inequality.data.datasets[0].data = sampledData.giniCoefficients;
charts.inequality.data.datasets[1].data = sampledData.top10Share;
charts.inequality.update('none');
}
@@ -1139,7 +1238,8 @@ async function onSimulationComplete(data) {
// Fetch complete simulation data and populate charts
try {
debugLog('Fetching complete simulation data...');
const response = await window.MarkovEconomics.utils.apiRequest(
// Use enhanced API request with batching support
const response = await enhancedApiRequest(
`/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true`
);
@@ -1173,7 +1273,8 @@ async function onSimulationComplete(data) {
// Fallback: try to get distribution data from dedicated endpoint
try {
debugLog('Attempting fallback distribution fetch...');
const distResponse = await window.MarkovEconomics.utils.apiRequest(
// Use enhanced API request with batching support
const distResponse = await enhancedApiRequest(
`/api/simulation/${currentSimulation.id}/distribution?bins=10`
);
if (distResponse.histogram && distResponse.histogram.labels && distResponse.histogram.counts) {
@@ -1280,6 +1381,125 @@ function downloadFile(content, filename, contentType) {
window.URL.revokeObjectURL(url);
}
/**
* Calculate optimal bin count for distribution chart based on agent count
* @param {number} agentCount - Number of agents in the simulation
* @returns {number} - Optimal number of bins
*/
function getOptimalBinCount(agentCount) {
if (agentCount < 50) {
return Math.max(5, Math.floor(agentCount / 5));
} else if (agentCount < 200) {
return 10;
} else if (agentCount < 1000) {
return 15;
} else if (agentCount < 5000) {
return 20;
} else {
return 25; // Cap at 25 bins for very large simulations
}
}
/**
* Fetch distribution data with dynamic bin count using enhanced API requests
*/
async function fetchDistributionData() {
if (!currentSimulation.id) return null;
try {
// Calculate optimal bin count based on agent count
const binCount = getOptimalBinCount(currentSimulation.parameters.num_agents || 100);
// Use enhanced API request with batching support
const response = await enhancedApiRequest(
`/api/simulation/${currentSimulation.id}/distribution?bins=${binCount}`
);
return response.histogram;
} catch (error) {
debugLog('Error fetching distribution data', error);
return null;
}
}
/**
* Batch API requests to reduce network overhead
*/
class ApiRequestBatcher {
constructor() {
this.pendingRequests = [];
this.batchTimeout = null;
this.maxBatchSize = 5;
}
/**
* Add a request to the batch
*/
addRequest(url, options = {}) {
return new Promise((resolve, reject) => {
this.pendingRequests.push({ url, options, resolve, reject });
// If we've reached max batch size, flush immediately
if (this.pendingRequests.length >= this.maxBatchSize) {
this.flush();
} else if (!this.batchTimeout) {
// Otherwise, schedule a flush
this.batchTimeout = setTimeout(() => this.flush(), 50);
}
});
}
/**
* Flush all pending requests
*/
async flush() {
if (this.batchTimeout) {
clearTimeout(this.batchTimeout);
this.batchTimeout = null;
}
if (this.pendingRequests.length === 0) return;
// For now, we'll process requests individually since our API doesn't support batching
// In a real implementation, this would send a single batched request
const requests = [...this.pendingRequests];
this.pendingRequests = [];
// Process all requests
for (const request of requests) {
try {
const response = await fetch(request.url, request.options);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
request.resolve(data);
} catch (error) {
request.reject(error);
}
}
}
}
// Create a global instance
window.apiBatcher = new ApiRequestBatcher();
/**
* Enhanced API request function with batching support
*/
async function enhancedApiRequest(url, options = {}) {
// Use batching for certain types of requests
const shouldBatch = url.includes('/data') || url.includes('/distribution');
if (shouldBatch) {
return window.apiBatcher.addRequest(url, options);
} else {
// Fall back to regular API request for non-batchable requests
return window.MarkovEconomics.utils.apiRequest(url, options);
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
// Initial inequality warning update

View File

@@ -285,6 +285,24 @@ C' → C: 1 (consumption cycle)
</div>
</div>
<!-- Project Information -->
<div class="card shadow mb-4">
<div class="card-header bg-info text-white">
<h4 class="mb-0">📄 Project Information</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-12">
<ul class="list-unstyled">
<li class="mb-2"><strong>© 2025 Markus F.J. Busche</strong> &lt;elpatron@mailbox.org&gt;</li>
<li class="mb-2"><strong>License:</strong> <a href="https://opensource.org/licenses/MIT" target="_blank">MIT License</a></li>
<li class="mb-2"><strong>Project Source Code:</strong> <a href="https://gitea.elpatron.me/elpatron/capitalism-eats-the-world.git" target="_blank">https://gitea.elpatron.me/elpatron/capitalism-eats-the-world.git</a></li>
</ul>
</div>
</div>
</div>
</div>
<!-- Call to Action -->
<div class="text-center">
<a href="{{ url_for('main.index') }}" class="btn btn-primary btn-lg">

View File

@@ -1,9 +1,8 @@
services:
markov-economics:
build: .
# Remove direct port mapping since Caddy will handle external access
# ports:
# - "5000:5000"
ports:
- "5000:5000"
environment:
- FLASK_ENV=production
- FLASK_APP=run.py

10
run.py
View File

@@ -15,11 +15,5 @@ if __name__ == '__main__':
debug_mode = config_name == 'development'
port = int(os.getenv('PORT', 5000)) # Use PORT env var or default to 5000
# For production deployment with Gunicorn, SocketIO will be handled by Gunicorn
# For development, we use SocketIO's built-in server
if config_name == 'production':
# In production, Gunicorn will handle the server
# This is just a fallback
socketio.run(app, debug=False, host='0.0.0.0', port=port, allow_unsafe_werkzeug=True)
else:
socketio.run(app, debug=debug_mode, host='0.0.0.0', port=port)
# Run with SocketIO's built-in server for both development and production
socketio.run(app, debug=debug_mode, host='0.0.0.0', port=port, allow_unsafe_werkzeug=True)