From d3be8837bb12f8aad559bca34da854ac5e9d0727 Mon Sep 17 00:00:00 2001 From: Tasnim Zulfiqar Date: Mon, 11 Aug 2025 13:10:33 -0400 Subject: [PATCH 1/6] progress (meeting) --- src/App.jsx | 26 ++++++++++++++++++++++++++ src/backend.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/App.jsx b/src/App.jsx index a17d5aa..7af9104 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -22,6 +22,7 @@ import EventsTab from './components/EventsTab.jsx'; import GlobalVariablesTab from './components/GlobalVariablesTab.jsx'; import { makeEdge } from './components/CustomEdge'; import { nodeTypes } from './nodeConfig.js'; +import LogDock from './components/LogDock.jsx'; // * Declaring variables * @@ -51,6 +52,12 @@ const DnDFlow = () => { 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) => { @@ -634,6 +641,17 @@ const DnDFlow = () => { }; // 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, @@ -654,6 +672,8 @@ const DnDFlow = () => { 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); @@ -1801,6 +1821,12 @@ const DnDFlow = () => { )} + { setDockOpen(false); if (sseRef.current) sseRef.current.close(); }} + lines={logLines} + progress={null} + /> ); } diff --git a/src/backend.py b/src/backend.py index 33af927..eb2839c 100644 --- a/src/backend.py +++ b/src/backend.py @@ -18,6 +18,11 @@ # 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).""" @@ -84,6 +89,42 @@ def docstring_to_html(docstring): 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) + +ps = logging.getLogger('pathsim') +ps.setLevel(logging.INFO) +ps.propagate = True +ps.addHandler(qhandler) + +### log backend ends # Serve React frontend for production @app.route("/") From b379fbb1a39dd59b786ac3c4ca8938a983b7a287 Mon Sep 17 00:00:00 2001 From: Tasnim Zulfiqar Date: Mon, 11 Aug 2025 13:13:07 -0400 Subject: [PATCH 2/6] add file --- src/components/LogDock.jsx | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/components/LogDock.jsx diff --git a/src/components/LogDock.jsx b/src/components/LogDock.jsx new file mode 100644 index 0000000..08a5c6e --- /dev/null +++ b/src/components/LogDock.jsx @@ -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 ( +
+ {/* Header */} +
+ Simulation Logs + {typeof progress === 'number' && ( +
+
+
+ )} + +
+ + {/* Log Lines */} +
+ {lines.length ? lines.join('\n') : 'Waiting for output…'} +
+
+ ); +} From adbfb34e8cc45ebbd7509a454cad25432968961f Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 11 Aug 2025 13:17:41 -0400 Subject: [PATCH 3/6] formatting --- src/backend.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/backend.py b/src/backend.py index eb2839c..2ba877b 100644 --- a/src/backend.py +++ b/src/backend.py @@ -24,6 +24,7 @@ import logging from queue import Queue, Empty + def docstring_to_html(docstring): """Convert a Python docstring to HTML using docutils (like Sphinx does).""" if not docstring: @@ -91,18 +92,22 @@ def docstring_to_html(docstring): ### 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(): + 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: @@ -111,21 +116,23 @@ def emit(self, record): except Exception: pass + qhandler = QueueHandler() -qhandler.setLevel(logging.INFO) +qhandler.setLevel(logging.INFO) qhandler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) -root = logging.getLogger() +root = logging.getLogger() root.setLevel(logging.INFO) root.addHandler(qhandler) -ps = logging.getLogger('pathsim') +ps = logging.getLogger("pathsim") ps.setLevel(logging.INFO) -ps.propagate = True +ps.propagate = True ps.addHandler(qhandler) ### log backend ends + # Serve React frontend for production @app.route("/") def serve_frontend(): From 602c64334f261f0bdb263869d6de84fdd074a516 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 11 Aug 2025 13:35:16 -0400 Subject: [PATCH 4/6] added queue handler to pathsim logger --- src/backend.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/backend.py b/src/backend.py index 2ba877b..2f1a41f 100644 --- a/src/backend.py +++ b/src/backend.py @@ -125,11 +125,6 @@ def emit(self, record): root.setLevel(logging.INFO) root.addHandler(qhandler) -ps = logging.getLogger("pathsim") -ps.setLevel(logging.INFO) -ps.propagate = True -ps.addHandler(qhandler) - ### log backend ends @@ -354,6 +349,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) From 92de1b4263ddce4109012dfa7ff5204e5a8903e2 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 11 Aug 2025 13:49:15 -0400 Subject: [PATCH 5/6] no need for save graphs anymore --- Dockerfile | 3 --- src/backend.py | 4 ---- 2 files changed, 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 65ecadf..d1f1301 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/src/backend.py b/src/backend.py index 2f1a41f..713d3e2 100644 --- a/src/backend.py +++ b/src/backend.py @@ -86,10 +86,6 @@ 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 From 7f7e16663eaae4bcbccc8767bd71400169edc6b3 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 11 Aug 2025 13:51:12 -0400 Subject: [PATCH 6/6] removed unused requests --- src/backend.py | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/src/backend.py b/src/backend.py index 713d3e2..07dde98 100644 --- a/src/backend.py +++ b/src/backend.py @@ -257,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():