From 1f7c7ef866d11fa07e7ff4cb5e2501bb2b047069 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Fri, 1 Aug 2025 08:46:02 -0400 Subject: [PATCH 1/4] added Dockerfile --- Dockerfile | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/backend.py | 46 +++++++++++++++++++++++++++++++-------- 2 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..1b924b97 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +# Multi-stage build optimized for Google Cloud Run +FROM node:18-alpine AS frontend-build + +# Build frontend +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY . . +RUN npm run build + +# Python backend stage +FROM python:3.11-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Copy Python requirements and install +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Install gunicorn for production WSGI server +RUN pip install gunicorn + +# Copy backend source code +COPY src/ ./src/ +COPY *.py ./ + +# Copy built frontend from previous stage +COPY --from=frontend-build /app/dist ./dist + +# Create necessary directories +RUN mkdir -p saved_graphs plots + +# Create non-root user for security +RUN useradd --create-home --shell /bin/bash app \ + && chown -R app:app /app +USER app + +# Set environment variables +ENV FLASK_APP=src.backend +ENV FLASK_ENV=production +ENV PYTHONPATH=/app +ENV PORT=8080 + +# Expose port (Cloud Run uses PORT env variable) +EXPOSE $PORT + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:$PORT/health || exit 1 + +# Use gunicorn for production +CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 src.backend:app diff --git a/src/backend.py b/src/backend.py index f5e5db83..e060ed44 100644 --- a/src/backend.py +++ b/src/backend.py @@ -12,12 +12,20 @@ from .pathsim_utils import make_pathsim_model from pathsim.blocks import Scope -app = Flask(__name__) -CORS( - app, - resources={r"/*": {"origins": "http://localhost:5173"}}, - supports_credentials=True, -) +# Configure Flask app for Cloud Run +app = Flask(__name__, static_folder='../dist', static_url_path='') + +# Configure CORS based on environment +if os.getenv('FLASK_ENV') == 'production': + # Production: Allow all origins for Cloud Run + CORS(app) +else: + # Development: Only allow localhost + CORS( + app, + resources={r"/*": {"origins": "http://localhost:5173"}}, + supports_credentials=True, + ) # Creates directory for saved graphs @@ -25,8 +33,17 @@ os.makedirs(SAVE_DIR, exist_ok=True) -# Health check endpoint for CI/CD -@app.route("/", methods=["GET"]) +# Serve React frontend for production +@app.route('/') +def serve_frontend(): + """Serve the React frontend in production.""" + if os.getenv('FLASK_ENV') == 'production': + return app.send_static_file('index.html') + else: + return jsonify({"message": "Fuel Cycle Simulator API", "status": "running"}) + + +# Health check endpoint for Cloud Run @app.route("/health", methods=["GET"]) def health_check(): return jsonify( @@ -165,5 +182,16 @@ def run_pathsim(): return jsonify({"success": False, "error": f"Server error: {str(e)}"}), 500 +# Catch-all route for React Router (SPA routing) +@app.route('/') +def catch_all(path): + """Serve React app for all routes in production (for client-side routing).""" + if os.getenv('FLASK_ENV') == 'production': + return app.send_static_file('index.html') + else: + return jsonify({"error": "Route not found"}), 404 + + if __name__ == "__main__": - app.run(port=8000, debug=True) + port = int(os.getenv('PORT', 8000)) + app.run(host='0.0.0.0', port=port, debug=os.getenv('FLASK_ENV') != 'production') From 1ccccf90f90d0e5bc4492d49cc217d7cd369f768 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Fri, 1 Aug 2025 08:50:43 -0400 Subject: [PATCH 2/4] removed --only=production --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1b924b97..6f52bd63 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM node:18-alpine AS frontend-build # Build frontend WORKDIR /app COPY package*.json ./ -RUN npm ci --only=production +RUN npm ci COPY . . RUN npm run build From ca1c6caa2fe20ef3e5fde4d44456dc6e807fe903 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Fri, 1 Aug 2025 09:48:14 -0400 Subject: [PATCH 3/4] handle production/dev deployment --- src/App.jsx | 5 +++-- src/backend.py | 31 ++++++++++++++++++------------- src/config.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 src/config.js diff --git a/src/App.jsx b/src/App.jsx index aee0c912..69824fb8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -11,6 +11,7 @@ import { import '@xyflow/react/dist/style.css'; import './App.css'; import Plot from 'react-plotly.js'; +import { getApiEndpoint } from './config.js'; import ContextMenu from './ContextMenu.jsx'; @@ -294,7 +295,7 @@ export default function App() { globalVariables }; - const response = await fetch('http://localhost:8000/convert-to-python', { + const response = await fetch(getApiEndpoint('/convert-to-python'), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -364,7 +365,7 @@ export default function App() { globalVariables }; - const response = await fetch('http://localhost:8000/run-pathsim', { + const response = await fetch(getApiEndpoint('/run-pathsim'), { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/backend.py b/src/backend.py index e060ed44..1c454b43 100644 --- a/src/backend.py +++ b/src/backend.py @@ -13,17 +13,22 @@ from pathsim.blocks import Scope # Configure Flask app for Cloud Run -app = Flask(__name__, static_folder='../dist', static_url_path='') +app = Flask(__name__, static_folder="../dist", static_url_path="") # Configure CORS based on environment -if os.getenv('FLASK_ENV') == 'production': - # Production: Allow all origins for Cloud Run - CORS(app) +if os.getenv("FLASK_ENV") == "production": + # Production: Allow Cloud Run domains and common domains + CORS(app, + resources={r"/*": { + "origins": ["*"], # Allow all origins for Cloud Run + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allow_headers": ["Content-Type", "Authorization"] + }}) else: # Development: Only allow localhost CORS( app, - resources={r"/*": {"origins": "http://localhost:5173"}}, + resources={r"/*": {"origins": ["http://localhost:5173", "http://localhost:3000"]}}, supports_credentials=True, ) @@ -34,11 +39,11 @@ # Serve React frontend for production -@app.route('/') +@app.route("/") def serve_frontend(): """Serve the React frontend in production.""" - if os.getenv('FLASK_ENV') == 'production': - return app.send_static_file('index.html') + if os.getenv("FLASK_ENV") == "production": + return app.send_static_file("index.html") else: return jsonify({"message": "Fuel Cycle Simulator API", "status": "running"}) @@ -183,15 +188,15 @@ def run_pathsim(): # Catch-all route for React Router (SPA routing) -@app.route('/') +@app.route("/") def catch_all(path): """Serve React app for all routes in production (for client-side routing).""" - if os.getenv('FLASK_ENV') == 'production': - return app.send_static_file('index.html') + if os.getenv("FLASK_ENV") == "production": + return app.send_static_file("index.html") else: return jsonify({"error": "Route not found"}), 404 if __name__ == "__main__": - port = int(os.getenv('PORT', 8000)) - app.run(host='0.0.0.0', port=port, debug=os.getenv('FLASK_ENV') != 'production') + port = int(os.getenv("PORT", 8000)) + app.run(host="0.0.0.0", port=port, debug=os.getenv("FLASK_ENV") != "production") diff --git a/src/config.js b/src/config.js new file mode 100644 index 00000000..e6f1cc21 --- /dev/null +++ b/src/config.js @@ -0,0 +1,28 @@ +// API configuration for development and production +const API_CONFIG = { + development: { + baseUrl: 'http://localhost:8000' + }, + production: { + baseUrl: '' // Use relative URLs in production (same domain) + } +}; + +// Get the current environment +const getCurrentEnvironment = () => { + return process.env.NODE_ENV === 'production' ? 'production' : 'development'; +}; + +// Get the API base URL for the current environment +export const getApiUrl = () => { + const env = getCurrentEnvironment(); + return API_CONFIG[env].baseUrl; +}; + +// Helper function to construct full API endpoint URLs +export const getApiEndpoint = (endpoint) => { + const baseUrl = getApiUrl(); + return `${baseUrl}${endpoint}`; +}; + +export default API_CONFIG; From da9143cc3d20031d2a4c7ba8fa0fe0512d80e31a Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Fri, 1 Aug 2025 09:53:39 -0400 Subject: [PATCH 4/4] CORS fixes --- src/backend.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/backend.py b/src/backend.py index 1c454b43..41d8dd97 100644 --- a/src/backend.py +++ b/src/backend.py @@ -18,17 +18,23 @@ # Configure CORS based on environment if os.getenv("FLASK_ENV") == "production": # Production: Allow Cloud Run domains and common domains - CORS(app, - resources={r"/*": { - "origins": ["*"], # Allow all origins for Cloud Run - "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - "allow_headers": ["Content-Type", "Authorization"] - }}) + CORS( + app, + resources={ + r"/*": { + "origins": ["*"], # Allow all origins for Cloud Run + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allow_headers": ["Content-Type", "Authorization"], + } + }, + ) else: # Development: Only allow localhost CORS( app, - resources={r"/*": {"origins": ["http://localhost:5173", "http://localhost:3000"]}}, + resources={ + r"/*": {"origins": ["http://localhost:5173", "http://localhost:3000"]} + }, supports_credentials=True, )