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 ```bash
# Build and start the application # Build and start the application
docker-compose up --build docker compose up --build
# Run in detached mode # Run in detached mode
docker-compose up -d --build docker compose up -d --build
# Stop the application # Stop the application
docker-compose down docker compose down
``` ```
### Using Docker directly ### Using Docker directly

View File

@@ -36,5 +36,5 @@ EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ 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 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 # Run the application with Flask's built-in server
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--worker-class", "gevent", "--worker-connections", "1000", "--timeout", "30", "wsgi:app"] 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: 1. Clone the repository:
```bash ```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 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: 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 2. Ensure your domain points to your server's IP address
3. Run with Caddy: 3. Run with Caddy:
```bash ```bash
docker-compose up -d 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 ## Documentation

View File

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

View File

@@ -139,19 +139,29 @@ def start_simulation(simulation_id: str):
try: try:
# Run simulation with progress updates # Run simulation with progress updates
total_iterations = simulation.parameters.iterations 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): for i in range(total_iterations):
if not simulation.is_running: if not simulation.is_running:
break break
snapshot = simulation.step() snapshot = simulation.step()
# Emit progress update every 10 iterations or at milestones # Emit progress update at dynamic intervals for better performance
if i % 10 == 0 or i == total_iterations - 1: current_time = time.time()
# Get distribution data for real-time chart updates if (i % max(1, total_iterations // 100) == 0 or # Every 1% of iterations
bin_labels, bin_counts = simulation.get_wealth_histogram(10) i == total_iterations - 1 or # Last iteration
current_time - last_update_time >= update_interval): # Time-based update
# Debug logging # Get distribution data for real-time chart updates (but less frequently)
print(f"DEBUG: Sending distribution data - labels: {bin_labels}, counts: {bin_counts}") 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 = { progress_data = {
'simulation_id': simulation_id, 'simulation_id': simulation_id,
@@ -161,18 +171,19 @@ def start_simulation(simulation_id: str):
'total_wealth': snapshot.total_wealth, 'total_wealth': snapshot.total_wealth,
'gini_coefficient': snapshot.gini_coefficient, 'gini_coefficient': snapshot.gini_coefficient,
'capital_share': snapshot.capital_share, 'capital_share': snapshot.capital_share,
'wealth_concentration_top10': snapshot.wealth_concentration_top10, 'wealth_concentration_top10': snapshot.wealth_concentration_top10
'distribution': {
'labels': bin_labels,
'counts': bin_counts
}
} }
# Only include distribution data when we have it
if distribution_data:
progress_data['distribution'] = distribution_data
socketio.emit('simulation_progress', progress_data, socketio.emit('simulation_progress', progress_data,
room=f'simulation_{simulation_id}') room=f'simulation_{simulation_id}')
last_update_time = current_time
# Small delay to allow real-time visualization # Adaptive delay based on performance
time.sleep(0.01) time.sleep(max(0.001, 0.01 * (total_iterations / 1000)))
# Mark as completed # Mark as completed
simulation.is_running = False simulation.is_running = False
@@ -363,11 +374,21 @@ def get_wealth_distribution(simulation_id: str):
if num_bins < 1 or num_bins > 50: if num_bins < 1 or num_bins > 50:
num_bins = 10 # Default to 10 bins 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 # Get histogram data
bin_labels, bin_counts = simulation.get_wealth_histogram(num_bins) bin_labels, bin_counts = simulation.get_wealth_histogram(num_bins)
# Debug logging # 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 = { response_data = {
'simulation_id': simulation_id, '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) return jsonify(response_data)
except Exception as e: except Exception as e:

View File

@@ -191,10 +191,14 @@ def test_simulation():
def debug_info(): def debug_info():
""" """
Debug endpoint to verify deployment and test functionality. Debug endpoint to verify deployment and test functionality.
Only available in development mode.
Returns:
JSON response with debug information
""" """
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 import os
from datetime import datetime from datetime import datetime
from app.models.economic_model import EconomicSimulation, SimulationParameters from app.models.economic_model import EconomicSimulation, SimulationParameters

View File

@@ -26,13 +26,13 @@ function initializeSocket() {
window.MarkovEconomics.socket = io(); window.MarkovEconomics.socket = io();
window.MarkovEconomics.socket.on('connect', function() { window.MarkovEconomics.socket.on('connect', function() {
console.log('Connected to server'); // console.log('Connected to server');
window.MarkovEconomics.isConnected = true; window.MarkovEconomics.isConnected = true;
updateConnectionStatus(true); updateConnectionStatus(true);
}); });
window.MarkovEconomics.socket.on('disconnect', function() { window.MarkovEconomics.socket.on('disconnect', function() {
console.log('Disconnected from server'); // console.log('Disconnected from server');
window.MarkovEconomics.isConnected = false; window.MarkovEconomics.isConnected = false;
updateConnectionStatus(false); updateConnectionStatus(false);
}); });
@@ -50,9 +50,9 @@ function initializeSocket() {
function updateConnectionStatus(connected) { function updateConnectionStatus(connected) {
// You can add a connection status indicator here if needed // You can add a connection status indicator here if needed
if (connected) { if (connected) {
console.log('✅ Real-time connection established'); // console.log('✅ Real-time connection established');
} else { } 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'); 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() { document.addEventListener('visibilitychange', function() {
if (document.hidden) { if (document.hidden) {
console.log('Page hidden - pausing updates'); // console.log('Page hidden - pausing updates');
} else { } 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 // Debug flag - can be enabled even in production for troubleshooting
const DEBUG_DISTRIBUTION = true; const DEBUG_DISTRIBUTION = false;
/** /**
* Debug logger that works in production * Debug logger that works in production
@@ -676,7 +676,7 @@ function initializeRealtimeUpdates() {
// Simulation progress updates // Simulation progress updates
socket.on('simulation_progress', function(data) { socket.on('simulation_progress', function(data) {
debugLog('Received real-time progress update'); debugLog('Received real-time progress update');
updateSimulationProgress(data); onSimulationProgress(data);
}); });
// Simulation completion // Simulation completion
@@ -714,13 +714,14 @@ function startFallbackPolling() {
} }
try { 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` `/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true`
); );
if (response.latest_snapshot) { if (response.latest_snapshot) {
// Simulate progress update // Simulate progress update
updateSimulationProgress({ onSimulationProgress({
iteration: response.latest_snapshot.iteration, iteration: response.latest_snapshot.iteration,
total_wealth: response.latest_snapshot.total_wealth, total_wealth: response.latest_snapshot.total_wealth,
gini_coefficient: response.latest_snapshot.gini_coefficient, gini_coefficient: response.latest_snapshot.gini_coefficient,
@@ -772,8 +773,8 @@ async function startSimulation() {
// Update UI state // Update UI state
updateUIState('starting'); updateUIState('starting');
// Create simulation // Create simulation using enhanced API request
const createResponse = await window.MarkovEconomics.utils.apiRequest('/api/simulation', { const createResponse = await enhancedApiRequest('/api/simulation', {
method: 'POST', method: 'POST',
body: JSON.stringify(parameters) body: JSON.stringify(parameters)
}); });
@@ -796,8 +797,8 @@ async function startSimulation() {
debugLog('WebSocket not available, will use fallback polling'); debugLog('WebSocket not available, will use fallback polling');
} }
// Start simulation // Start simulation using enhanced API request
await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/start`, { await enhancedApiRequest(`/api/simulation/${currentSimulation.id}/start`, {
method: 'POST' method: 'POST'
}); });
@@ -825,7 +826,8 @@ async function stopSimulation() {
if (!currentSimulation.id) return; if (!currentSimulation.id) return;
try { 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' 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) { function getAdaptiveUpdateInterval() {
// Update progress bar if (!currentSimulation.parameters.num_agents || !currentSimulation.parameters.iterations) {
const progressBar = document.getElementById('progressBar'); return 50; // Default to 50ms
const progressText = document.getElementById('progressText'); }
if (progressBar && progressText) { const agentCount = currentSimulation.parameters.num_agents;
const percentage = data.progress_percentage || 0; 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 + '%'; progressBar.style.width = percentage + '%';
progressText.textContent = percentage.toFixed(1) + '%'; progressText.textContent = percentage.toFixed(1) + '%';
} }
// Update charts and metrics // Update charts and metrics only if we have new data
if (data.iteration !== undefined) { if (data.iteration !== undefined) {
currentSimulation.data.iterations.push(data.iteration); // Only update time series data occasionally to improve performance
currentSimulation.data.totalWealth.push(data.total_wealth || 0); const shouldUpdateSeries = currentSimulation.data.iterations.length === 0 ||
currentSimulation.data.giniCoefficients.push(data.gini_coefficient || 0); data.iteration % Math.max(1, Math.floor(currentSimulation.parameters.iterations / 200)) === 0 ||
currentSimulation.data.capitalShare.push(data.capital_share || 0); data.iteration === (currentSimulation.parameters.iterations - 1);
currentSimulation.data.top10Share.push(data.wealth_concentration_top10 || 0);
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 // 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 && if (data.distribution &&
Array.isArray(data.distribution.labels) && Array.isArray(data.distribution.labels) &&
Array.isArray(data.distribution.counts) && Array.isArray(data.distribution.counts) &&
data.distribution.labels.length > 0 && data.distribution.labels.length > 0 &&
data.distribution.counts.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 // Store the distribution data properly
currentSimulation.data.distribution.labels = [...data.distribution.labels]; currentSimulation.data.distribution.labels = [...data.distribution.labels];
currentSimulation.data.distribution.counts = [...data.distribution.counts]; currentSimulation.data.distribution.counts = [...data.distribution.counts];
} else { } else if (data.iteration % Math.max(1, Math.floor(currentSimulation.parameters.iterations / 50)) === 0) {
debugLog('No valid distribution data in progress update'); // 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); 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() { function updateCharts() {
// Determine maximum points based on agent count for better performance
const maxChartPoints = currentSimulation.parameters.num_agents > 1000 ? 100 : 200;
// Wealth Evolution Chart // Wealth Evolution Chart
if (charts.wealthEvolution) { if (charts.wealthEvolution) {
charts.wealthEvolution.data.labels = currentSimulation.data.iterations; // Sample data for large datasets
charts.wealthEvolution.data.datasets[0].data = currentSimulation.data.totalWealth; 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'); charts.wealthEvolution.update('none');
} }
// Inequality Chart // Inequality Chart
if (charts.inequality) { if (charts.inequality) {
charts.inequality.data.labels = currentSimulation.data.iterations; // Sample data for large datasets
charts.inequality.data.datasets[0].data = currentSimulation.data.giniCoefficients; const sampledData = sampleTimeSeriesData({
charts.inequality.data.datasets[1].data = currentSimulation.data.top10Share; 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'); charts.inequality.update('none');
} }
@@ -1139,7 +1238,8 @@ async function onSimulationComplete(data) {
// Fetch complete simulation data and populate charts // Fetch complete simulation data and populate charts
try { try {
debugLog('Fetching complete simulation data...'); 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` `/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 // Fallback: try to get distribution data from dedicated endpoint
try { try {
debugLog('Attempting fallback distribution fetch...'); 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` `/api/simulation/${currentSimulation.id}/distribution?bins=10`
); );
if (distResponse.histogram && distResponse.histogram.labels && distResponse.histogram.counts) { if (distResponse.histogram && distResponse.histogram.labels && distResponse.histogram.counts) {
@@ -1280,6 +1381,125 @@ function downloadFile(content, filename, contentType) {
window.URL.revokeObjectURL(url); 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 // Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Initial inequality warning update // Initial inequality warning update

View File

@@ -285,6 +285,24 @@ C' → C: 1 (consumption cycle)
</div> </div>
</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 --> <!-- Call to Action -->
<div class="text-center"> <div class="text-center">
<a href="{{ url_for('main.index') }}" class="btn btn-primary btn-lg"> <a href="{{ url_for('main.index') }}" class="btn btn-primary btn-lg">

View File

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

10
run.py
View File

@@ -15,11 +15,5 @@ if __name__ == '__main__':
debug_mode = config_name == 'development' debug_mode = config_name == 'development'
port = int(os.getenv('PORT', 5000)) # Use PORT env var or default to 5000 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 # Run with SocketIO's built-in server for both development and production
# For development, we use SocketIO's built-in server socketio.run(app, debug=debug_mode, host='0.0.0.0', port=port, allow_unsafe_werkzeug=True)
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)