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
3 changes: 0 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ RUN pip install gunicorn
# 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
Expand Down
26 changes: 26 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import GlobalVariablesTab from './components/GlobalVariablesTab.jsx';
import { makeEdge } from './components/CustomEdge';
import { nodeTypes } from './nodeConfig.js';
import LogDock from './components/LogDock.jsx';

// * Declaring variables *

Expand Down Expand Up @@ -51,6 +52,12 @@
const { screenToFlowPosition } = useReactFlow();
const [type] = useDnD();

// for the log dock
const [dockOpen, setDockOpen] = useState(false);
const [logLines, setLogLines] = useState([]);
const sseRef = useRef(null);
const append = (line) => setLogLines((prev) => [...prev, line]);

// const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), []);

const onDragOver = useCallback((event) => {
Expand All @@ -59,7 +66,7 @@
}, []);

const onDragStart = (event, nodeType) => {
setType(nodeType);

Check failure on line 69 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

'setType' is not defined

Check failure on line 69 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

'setType' is not defined
event.dataTransfer.setData('text/plain', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
Expand Down Expand Up @@ -259,7 +266,7 @@
setNodes((nds) => [...nds, newNode]);
setNodeCounter((count) => count + 1);
},
[screenToFlowPosition, type, nodeCounter, fetchDefaultValues, setDefaultValues, setNodes, setNodeCounter],

Check warning on line 269 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.11)

React Hook useCallback has an unnecessary dependency: 'setDefaultValues'. Either exclude it or remove the dependency array

Check warning on line 269 in src/App.jsx

View workflow job for this annotation

GitHub Actions / test (20.x, 3.10)

React Hook useCallback has an unnecessary dependency: 'setDefaultValues'. Either exclude it or remove the dependency array
);

// Function to save a graph to computer with "Save As" dialog
Expand Down Expand Up @@ -634,6 +641,17 @@
};
// Function to run pathsim simulation
const runPathsim = async () => {
setDockOpen(true);
setLogLines([]);

if (sseRef.current) sseRef.current.close();
const es = new EventSource(getApiEndpoint('/logs/stream'));
sseRef.current = es;

es.addEventListener('start', () => append('log stream connected…'));
es.onmessage = (evt) => append(evt.data);
es.onerror = () => { append('log stream error'); es.close(); sseRef.current = null; };

try {
const graphData = {
nodes,
Expand All @@ -654,6 +672,8 @@

const result = await response.json();

if (sseRef.current) { sseRef.current.close(); sseRef.current = null; }

if (result.success) {
// Store results and switch to results tab
setSimulationResults(result.plot);
Expand Down Expand Up @@ -984,7 +1004,7 @@
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [selectedEdge, selectedNode, copiedNode, duplicateNode, setCopyFeedback]);

Check warning on line 1007 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

Check warning on line 1007 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

return (
<div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column' }}>
Expand Down Expand Up @@ -1801,6 +1821,12 @@
</div>
</div>
)}
<LogDock
open={dockOpen}
onClose={() => { setDockOpen(false); if (sseRef.current) sseRef.current.close(); }}
lines={logLines}
progress={null}
/>
</div>
);
}
Expand Down
85 changes: 46 additions & 39 deletions src/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
# Sphinx imports for docstring processing
from docutils.core import publish_parts

# imports for logging progress
from flask import Response, stream_with_context
import time
import logging
from queue import Queue, Empty


def docstring_to_html(docstring):
"""Convert a Python docstring to HTML using docutils (like Sphinx does)."""
Expand Down Expand Up @@ -80,9 +86,42 @@ def docstring_to_html(docstring):
)


# Creates directory for saved graphs
SAVE_DIR = "saved_graphs"
os.makedirs(SAVE_DIR, exist_ok=True)
### for capturing logs from pathsim


@app.get("/logs/stream")
def logs_stream():
def gen():
yield "retry: 500\n\n"
while True:
line = log_queue.get()
for chunk in line.replace("\r", "\n").splitlines():
yield f"data: {chunk}\n\n"

return Response(gen(), mimetype="text/event-stream")


log_queue = Queue()


class QueueHandler(logging.Handler):
def emit(self, record):
try:
msg = self.format(record)
log_queue.put_nowait(msg)
except Exception:
pass


qhandler = QueueHandler()
qhandler.setLevel(logging.INFO)
qhandler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))

root = logging.getLogger()
root.setLevel(logging.INFO)
root.addHandler(qhandler)

### log backend ends


# Serve React frontend for production
Expand Down Expand Up @@ -218,42 +257,6 @@ def get_docs(node_type):
return jsonify({"error": f"Could not get docs for {node_type}: {str(e)}"}), 400


# Function to save graphs
@app.route("/save", methods=["POST"])
def save_graph():
data = request.json
filename = data.get(
"filename", "file_1"
) # sets file_1 as default filename if not provided
graph_data = data.get("graph")

# Enforces .json extension and valid filenames
valid_name = f"{filename}.json" if not filename.endswith(".json") else filename
file_path = os.path.join(SAVE_DIR, valid_name)

with open(file_path, "w") as f:
json.dump(graph_data, f, indent=2)

return jsonify({"message": f"Graph saved as {valid_name}"})


# Function to load saved graphs
@app.route("/load", methods=["POST"])
def load_graph():
data = request.json
filename = data.get("filename")
validname = filename if not filename.endswith(".json") else filename[:-5]
filepath = os.path.join(SAVE_DIR, f"{validname}.json")

if not os.path.exists(filepath):
return jsonify({"error": "File not found"}), 404

with open(filepath, "r") as f:
graph_data = json.load(f)

return jsonify(graph_data)


# Function to convert graph to Python script
@app.route("/convert-to-python", methods=["POST"])
def convert_to_python():
Expand Down Expand Up @@ -306,6 +309,10 @@ def run_pathsim():

my_simulation, duration = make_pathsim_model(graph_data)

# get the pathsim logger and add the queue handler
logger = my_simulation.logger
logger.addHandler(qhandler)

# Run the simulation
my_simulation.run(duration)

Expand Down
48 changes: 48 additions & 0 deletions src/components/LogDock.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// components/LogDock.jsx
import React from 'react';

export default function LogDock({ open, onClose, lines, progress }) {
if (!open) return null; // don’t render if it's closed

return (
<div style={{
position: 'fixed',
left: 0, right: 0, bottom: 0,
height: '30vh',
background: '#111',
color: '#ddd',
borderTop: '1px solid #333',
display: 'flex',
flexDirection: 'column',
zIndex: 1000
}}>
{/* Header */}
<div style={{ padding: '8px', display: 'flex', alignItems: 'center', gap: 12 }}>
<strong>Simulation Logs</strong>
{typeof progress === 'number' && (
<div style={{ flex: 1, height: 6, background: '#333', borderRadius: 3 }}>
<div style={{
width: `${progress}%`,
height: '100%',
background: '#78A083',
borderRadius: 3
}} />
</div>
)}
<button onClick={onClose} style={{ marginLeft: 'auto' }}>Close</button>
</div>

{/* Log Lines */}
<div style={{
flex: 1,
overflow: 'auto',
fontFamily: 'monospace',
fontSize: 12,
padding: '8px 12px',
whiteSpace: 'pre-wrap'
}}>
{lines.length ? lines.join('\n') : 'Waiting for output…'}
</div>
</div>
);
}
Loading