Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
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
5 changes: 3 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
Background,
useNodesState,
useEdgesState,
addEdge,

Check failure on line 9 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'addEdge' is defined but never used. Allowed unused vars must match /^[A-Z_]/u

Check failure on line 9 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'addEdge' is defined but never used. Allowed unused vars must match /^[A-Z_]/u
} from '@xyflow/react';
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';

Expand Down Expand Up @@ -294,7 +295,7 @@
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',
Expand Down Expand Up @@ -364,7 +365,7 @@
globalVariables
};

const response = await fetch('http://localhost:8000/run-pathsim', {
const response = await fetch(getApiEndpoint('/run-pathsim'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -838,7 +839,7 @@
borderRadius: 5,
cursor: 'pointer',
}}
onClick={() => setActiveTab('globals')}

Check warning on line 842 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

React Hook useEffect has missing dependencies: 'deleteSelectedEdge' and 'deleteSelectedNode'. Either include them or remove the dependency array

Check warning on line 842 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

React Hook useEffect has missing dependencies: 'deleteSelectedEdge' and 'deleteSelectedNode'. Either include them or remove the dependency array
>
Global Variables
</button>
Expand Down
57 changes: 48 additions & 9 deletions src/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,49 @@
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 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", "http://localhost:3000"]}
},
supports_credentials=True,
)


# Creates directory for saved graphs
SAVE_DIR = "saved_graphs"
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(
Expand Down Expand Up @@ -165,5 +193,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("/<path:path>")
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")
28 changes: 28 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -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';

Check failure on line 13 in src/config.js

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'process' is not defined

Check failure on line 13 in src/config.js

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'process' is not defined
};

// 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;
Loading