diff --git a/README.md b/README.md index 1314d08..d3f213d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Capabilities2 UI +# Event Logger UI A Node.js + React-based web application that connects to [`foxglove-rosbridge`](https://github.com/foxglove/ros-foxglove-bridge) and visualizes `/events` messages published in ROS2 using the CDR encoding format. @@ -9,29 +9,39 @@ This app allows: - REST API for historical data access -## 🚀 How to Run - -### 1. Prerequisites +## Prerequisites - Node.js ≥ 18.x - MongoDB (running locally or via Atlas) - ROS2 Capabilities2 system publishing to `/events` via [foxglove-bridge](https://github.com/foxglove/ros-foxglove-bridge) -### 2. Clone and Setup +## Clone and Setup ```bash -git clone https://github.com/CollaborativeRoboticsLab/capabilities2-ui.git -cd capabilities2-ui +git clone https://github.com/CollaborativeRoboticsLab/event_logger_ui.git +cd event_logger_ui ``` -### 3. Backend +## Docker based Deployment + +Make sure you have docker installed. Then, + +```sh +docker compose up +``` + +## Pure Deployment + +### Backend + +If you want to modify or start the backend seperately, run following commands ```bash cd backend npm install # Setup MongoDB URI -echo "MONGO_URI=mongodb://localhost:27017/capabilities2" > .env +echo "MONGO_URI=mongodb://localhost:27017/event_logger" > .env # Start the backend (REST API + WebSocket + Foxglove client) node server.js @@ -52,7 +62,9 @@ node server.js ``` -### 4. Frontend +### Frontend + +If you want to modify or start the backend seperately, run following commands ```bash cd ../frontend diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..af69b10 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,9 @@ +node_modules +npm-debug.log +Dockerfile +.dockerignore +.git +.gitignore +.env +*.md +*.log diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..6d07cc3 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,21 @@ +# Use a lightweight Node.js base image +FROM node:18-alpine + +# Set working directory +WORKDIR /app + +# Copy package files and install dependencies +COPY package.json package-lock.json ./ +RUN npm ci + +# Copy backend source code +COPY . . + +# Set environment variables (override in production) +ENV PORT=5000 + +# Expose backend port +EXPOSE 5000 + +# Start server +CMD ["node", "server.js"] \ No newline at end of file diff --git a/backend/decoders/capabilitiesEventDecoder.js b/backend/decoders/EventDecoder.js similarity index 93% rename from backend/decoders/capabilitiesEventDecoder.js rename to backend/decoders/EventDecoder.js index 317a886..9b5a111 100644 --- a/backend/decoders/capabilitiesEventDecoder.js +++ b/backend/decoders/EventDecoder.js @@ -19,7 +19,7 @@ const TYPE_ENUM_MAP = { 5: "RUNNER_EVENT", }; -function decodeCapabilityEvent(data) { +function decodeEvent(data) { const reader = new CdrReader(data); try { const header = { @@ -36,13 +36,13 @@ function decodeCapabilityEvent(data) { capability: reader.string(), provider: reader.string(), parameters: reader.string(), - }; + } const target = { capability: reader.string(), provider: reader.string(), parameters: reader.string(), - }; + } const thread_id = reader.int8(); @@ -69,4 +69,4 @@ function decodeCapabilityEvent(data) { } } -module.exports = decodeCapabilityEvent; +module.exports = decodeEvent; diff --git a/backend/decoders/index.js b/backend/decoders/index.js index dec0ece..a374f9c 100644 --- a/backend/decoders/index.js +++ b/backend/decoders/index.js @@ -1,5 +1,5 @@ module.exports = { - "/events": require("./capabilitiesEventDecoder"), + "/events": require("./EventDecoder"), // future topics can be added like: // "/sensor_data": require("./sensorDataDecoder"), diff --git a/backend/foxgloveClient.js b/backend/foxgloveClient.js index bde33bb..d02c11c 100644 --- a/backend/foxgloveClient.js +++ b/backend/foxgloveClient.js @@ -26,10 +26,10 @@ async function startFoxgloveClient(sessionId) { console.log(`[FoxgloveClient] Starting client for session: ${sessionId}`); const tryConnect = () => { - console.log("[FoxgloveClient] Attempting to connect to ws://0.0.0.0:8765..."); + console.log("[FoxgloveClient] Attempting to connect to ws://localhost:8765..."); const client = new FoxgloveClient({ - ws: new WebSocket("ws://0.0.0.0:8765", [FoxgloveClient.SUPPORTED_SUBPROTOCOL]), + ws: new WebSocket(process.env.ROSBRIDGE_URL || "ws://localhost:8765", [FoxgloveClient.SUPPORTED_SUBPROTOCOL]), }); const deserializers = new Map(); @@ -75,7 +75,7 @@ async function startFoxgloveClient(sessionId) { // Queue graph-related processing instead of direct function calls if (["RUNNER_DEFINE", "RUNNER_EVENT"].includes(event.type)) { - graphQueueManager.addToQueue(event, sessionId); + graphQueueManager.process(event, sessionId); } }) .catch((err) => @@ -84,7 +84,7 @@ async function startFoxgloveClient(sessionId) { }); client.on("error", (err) => { - console.error(`[FoxgloveClient] ❌ WebSocket error: ${err.message}`); + console.error(`[FoxgloveClient] ❌ WebSocket error for session ${sessionId}: ${err.message}`); }); client.on("close", () => { diff --git a/backend/models/Graph.js b/backend/models/Graph.js index 4914e24..bae9d4a 100644 --- a/backend/models/Graph.js +++ b/backend/models/Graph.js @@ -1,19 +1,19 @@ const mongoose = require("mongoose"); const nodeSchema = new mongoose.Schema({ - nodeId: { type: Number, required: true, unique: true }, + nodeId: { type: Number, required: true}, capability: String, provider: String, }, { _id: false }); const edgeSchema = new mongoose.Schema({ - edgeId: { type: Number, required: true, unique: true }, + edgeId: { type: Number, required: true}, sourceNodeID: { type: Number, required: true }, // Node ID of the source node targetNodeID: { type: Number, required: true }, // Node ID of the target node }, { _id: false }); const eventLogSchema = new mongoose.Schema({ - eventId: { type: Number, required: true, unique: true }, + eventId: { type: Number, required: true}, nodeId: { type: Number, default: null }, // Optional, can be null if not related to a node edgeId: { type: Number, default: null }, // Optional, can be null if not related to an edge nodeState: { @@ -27,7 +27,7 @@ const eventLogSchema = new mongoose.Schema({ const graphSchema = new mongoose.Schema({ graphId: { type: String, required: true, unique: true }, session: { type: mongoose.Schema.Types.ObjectId, ref: "Session", required: true }, - graphNo: { type: Number, required: true, unique: true }, + graphNo: { type: Number, required: true }, nodes: [nodeSchema], edges: [edgeSchema], eventLog: [eventLogSchema], @@ -35,4 +35,6 @@ const graphSchema = new mongoose.Schema({ completedAt: Date, }, { timestamps: true }); +graphSchema.index({ session: 1, graphNo: 1 }, { unique: true }); + module.exports = mongoose.model("Graph", graphSchema); diff --git a/backend/routes/events.js b/backend/routes/events.js index c32feb5..6b2f205 100644 --- a/backend/routes/events.js +++ b/backend/routes/events.js @@ -2,25 +2,19 @@ const express = require('express'); const router = express.Router(); const Event = require('../models/Event'); -// Save new event -router.post('/', async (req, res) => { - try { - const newEvent = new Event(req.body); - const saved = await newEvent.save(); - res.status(201).json(saved); - } catch (err) { - res.status(400).json({ error: err.message }); - } -}); - -// Get all events +// Get filtered events router.get('/', async (req, res) => { - try { - const events = await Event.find().sort({ createdAt: -1 }); - res.json(events); - } catch (err) { - res.status(500).json({ error: err.message }); + try { + const filter = {}; + + if (req.query.session) { + filter.session = req.query.session; } + + const events = await Event.find(filter).sort({ createdAt: -1 }); + res.json(events); + } catch (err) { + res.status(500).json({ error: err.message }); } }); module.exports = router; \ No newline at end of file diff --git a/backend/routes/graphs.js b/backend/routes/graphs.js index 241debf..6726460 100644 --- a/backend/routes/graphs.js +++ b/backend/routes/graphs.js @@ -8,6 +8,7 @@ router.post("/:sessionId/create", async (req, res) => { const { sessionId } = req.params; const graph = await createGraphForSession(sessionId); if (!graph) return res.status(404).json({ message: "No RUNNER_DEFINE events found" }); + console.info("Graph creation sucess",sessionId); res.json(graph); } catch (err) { console.error("Graph creation failed", err.message); @@ -17,7 +18,9 @@ router.post("/:sessionId/create", async (req, res) => { router.get("/:sessionId", async (req, res) => { try { - const graphs = await Graph.find({ session: req.params.sessionId }).sort({ graphNumber: 1 }); + const { sessionId } = req.params; + const graphs = await Graph.find({ session: sessionId }).sort({ graphNo: 1 }); + console.info("All graph count ",graphs.length); res.json(graphs); } catch (err) { res.status(500).json({ error: err.message }); @@ -27,7 +30,9 @@ router.get("/:sessionId", async (req, res) => { router.get("/:sessionId/count", async (req, res) => { try { - const count = await Graph.countDocuments({ session: req.params.sessionId }); + const { sessionId } = req.params; + const count = await Graph.countDocuments({ session: sessionId }); + console.info("Graph count for session", sessionId, "is", count); res.json({ count }); } catch (err) { res.status(500).json({ error: err.message }); @@ -35,22 +40,19 @@ router.get("/:sessionId/count", async (req, res) => { }); router.get("/:sessionId/:index", async (req, res) => { - const { sessionId, index } = req.params; try { - const graph = await Graph.findOne({ session: sessionId }) - .sort({ graphNo: 1 }) - .skip(Number(index)) - .limit(1); + const { sessionId, index } = req.params; - if (!graph) return res.status(404).json({ message: "Graph not found" }); + const graph = await Graph.findOne({ session: sessionId, graphNo: parseInt(index) + 1 }); + if (!graph) return res.status(404).json({ error: "Graph not found" }); + res.json(graph); } catch (err) { - res.status(500).json({ error: err.message }); + console.error("Error fetching graph:", err); + res.status(500).json({ error: "Server error" }); } }); - - module.exports = router; diff --git a/backend/routes/sessions.js b/backend/routes/sessions.js index 9af096c..09cdc50 100644 --- a/backend/routes/sessions.js +++ b/backend/routes/sessions.js @@ -30,8 +30,8 @@ router.post("/", async (req, res) => { console.log("[Session API] Created new session:", saved); if (req.query.listen === "true") { - console.log("[Session API] Triggering FoxgloveClient for session:", saved._id); - startFoxgloveClient(saved._id); + console.log("[Session API] Triggering FoxgloveClient for session with session id:", saved._id); + startFoxgloveClient(saved._id); // ✅ correct usage } res.status(201).json(saved); diff --git a/backend/server.js b/backend/server.js index f18b517..eb2bc42 100644 --- a/backend/server.js +++ b/backend/server.js @@ -22,7 +22,7 @@ app.use("/api/sessions", sessionRoutes); app.use("/api/events", eventRoutes); app.use("/api/graphs", graphRoutes); -mongoose.connect(process.env.MONGO_URI || "mongodb://localhost:27017/capabilities2", { +mongoose.connect(process.env.MONGO_URI || "mongodb://localhost:27017/event_logger", { useNewUrlParser: true, useUnifiedTopology: true, }); diff --git a/backend/utils/graphManage.js b/backend/utils/graphManage.js index 68e0005..958e125 100644 --- a/backend/utils/graphManage.js +++ b/backend/utils/graphManage.js @@ -4,18 +4,25 @@ const Event = require("../models/Event"); const activeGraphs = new Map(); // still exportable const graphNumbers = new Map(); // sessionId => graphId -/** Sets the active graph number for a given session ID. - * @param {string} sessionId - The ID of the session for which to set the active graph key. - * @param {number} graphNo - The graph number to set as active for the session. - * This function updates the activeGraphKeys map to associate the session ID with the specified graph number. - */ -async function updateGraphNumbers(sessionId) { - const graphCount = await Graph.countDocuments({ session: sessionId }); - graphNumbers.set(sessionId, graphCount + 1); - console.log(`[GraphManager] Updated graph number for session ${sessionId}: ${getGraphNumbers(sessionId)}`); +async function incrementGraphNumbers(sessionId) { + try { + const graphCount = await Graph.countDocuments({ session: sessionId }); + graphNumbers.set(sessionId, graphCount + 1); + console.log(`[GraphManager] Updated graph number for session ${sessionId}: ${getGraphNumbers(sessionId)}`); + } catch (err) { + console.error(`[GraphManager] ❌ Failed to update graph numbers for session ${sessionId}:`, err); + } } +/** Retrieves the graph number for a given session ID. + * @param {string} sessionId - The ID of the session for which to get the + * active graph number. + * @returns {number|null} The active graph number for the session, or null if no + * active graph exists. + * This function checks the graphNumbers map for the session ID and returns the associated graph number. + * If no graph number is found, it returns null. + */ function getGraphNumbers(sessionId) { return graphNumbers.get(sessionId) || null; } @@ -60,25 +67,6 @@ function getActiveGraph(sessionId) { } } -/** Sets the active graph for a given session ID. - * This function updates the activeGraphs map to associate the session ID with the specified graph. - * - * @param {string} sessionId - The ID of the session for which to set the active graph. - * @param {Graph} graph - The graph to set as active for the session. - * @returns {void} - * - * This function retrieves the active graph key for the session and sets the graph in the activeGraphs map. - * If no active graph key is found, it logs a warning. - */ -function setActiveGraph(sessionId, graph) { - const key = getActiveGraphKey(sessionId); - if (!key) { - console.warn(`[GraphManager] No active graph key for session ${sessionId}`); - return; - } - activeGraphs.set(key, graph); -} - /** Generates a graph structure from a list of events. This function processes the events of * type "RUNNER_DEFINE" to create nodes and edges. It initializes nodes for unique capabilities * and providers, creates edges between source and target nodes, and logs events. @@ -211,8 +199,17 @@ async function createGraphForSession(sessionId, events) { const { nodes, edges, eventLog } = generateGraph(events); - const graphKey = getActiveGraphKey(sessionId); + // Skip saving if graph is empty + if (!nodes.length && !edges.length && !eventLog.length) { + console.warn(`[GraphManager] Skipping empty graph save for session ${sessionId}`); + return null; + } + console.log(`[GraphManager] Creating graph for session ${sessionId} with ${nodes.length} nodes and ${edges.length} edges`); + + await incrementGraphNumbers(sessionId); + const graphNumber = getGraphNumbers(sessionId); + const graphKey = getActiveGraphKey(sessionId); console.log("Graph data for graph:", graphKey); console.log("Nodes:", nodes); @@ -228,11 +225,16 @@ async function createGraphForSession(sessionId, events) { eventLog: eventLog, }); - await graph.save(); + try { + await graph.save(); + activeGraphs.set(graphKey, graph); + return graph; - setActiveGraph(sessionId, graph); + } catch (err) { - return graph; + console.error(`[GraphManager] ❌ Failed to save graph ${graphKey}:`, err.message); + return null; + } } /** @@ -258,8 +260,6 @@ async function finalizeGraphForSession(sessionId) { activeGraphs.delete(key); console.log(`[GraphFinalizer] Finalized graph ${graph.graphId}`); - - updateGraphNumbers(sessionId); } /** Updates the graph with a runner event. @@ -299,89 +299,89 @@ async function updateGraphWithRunnerEvent(sessionId, event) { let eventId = graph.eventLog.length; - if (event.type === "RUNNER_EVENT") { - if (!sourceCapability || !sourceProvider || !targetCapability || !targetProvider) { - console.warn(`[GraphUpdater] Invalid event data for session ${sessionKey}:`, event); - return; - } - - if (graph.nodes.some(node => node.capability === sourceCapability && node.provider === sourceProvider)) { - sourceNodeID = graph.nodes.find(node => node.capability === sourceCapability && node.provider === sourceProvider).nodeId; - } else { - console.warn(`[GraphUpdater] Source node not found in graph for session ${sessionKey}`); - return; - } + if (!sourceCapability || !sourceProvider || !targetCapability || !targetProvider) { + console.warn(`[GraphUpdater] Invalid event data for session ${sessionId}:`, event); + return; + } - if (graph.nodes.some(node => node.capability === targetCapability && node.provider === targetProvider)) { - targetNodeID = graph.nodes.find(node => node.capability === targetCapability && node.provider === targetProvider).nodeId; - } else { - console.warn(`[GraphUpdater] Target node not found in graph for session ${sessionKey}`); - return; - } + if (graph.nodes.some(node => node.capability === sourceCapability && node.provider === sourceProvider)) { + sourceNodeID = graph.nodes.find(node => node.capability === sourceCapability && node.provider === sourceProvider).nodeId; + } else { + console.warn(`[GraphUpdater] Source node ${sourceProvider}/${sourceCapability} not found in graph for session ${sessionId}`); + return; + } - if (sourceNodeID !== null || targetNodeID !== null) { - edgeID = graph.edges.find(edge => edge.sourceNodeID === sourceNodeID && edge.targetNodeID === targetNodeID)?.edgeId; - if (edgeID === undefined) { - console.warn(`[GraphUpdater] Edge not found in graph for session ${sessionKey}:`, - `sourceNodeID=${sourceNodeID}, targetNodeID=${targetNodeID}`); - return; - } - } + if (graph.nodes.some(node => node.capability === targetCapability && node.provider === targetProvider)) { + targetNodeID = graph.nodes.find(node => node.capability === targetCapability && node.provider === targetProvider).nodeId; + } else { + console.warn(`[GraphUpdater] Target node ${targetCapability}/${targetProvider} not found in graph for session ${sessionId}`); + return; + } - if (event.event === "STARTED") { - // Handle STARTED event logic if needed (parallel execution) - sourceState = "executing"; - targetState = "executing"; - edgeActivated = true; - } else if (event.event === "STOPPED") { - // Handle STOPPED event logic if needed (external interruption, target is response kinda recovery) - sourceState = "failed"; - targetState = "executing"; - edgeActivated = true; - } else if (event.event === "FAILED") { - // Handle FAILED event logic if needed (failure usually due to an error, target is recovery) - sourceState = "failed"; - targetState = "executing "; - edgeActivated = true; - } else if (event.event === "SUCCEEDED") { - // Handle SUCCEEDED event logic if needed (target is sequential execution) - sourceState = "complete"; - targetState = "executing"; - edgeActivated = true; - } else { - // Handle other events or default case - console.warn(`[GraphUpdater] Unknown event type for session ${sessionKey}:`, event.event); + if (sourceNodeID !== null && targetNodeID !== null) { + edgeID = graph.edges.find(edge => edge.sourceNodeID === sourceNodeID && edge.targetNodeID === targetNodeID)?.edgeId; + if (edgeID === undefined) { + console.warn(`[GraphUpdater] Edge not found in graph for session ${sessionId}:`, + `sourceNodeID=${sourceNodeID}, targetNodeID=${targetNodeID}`); return; } + } - graph.eventLog.push({ - eventId: eventId, - nodeId: sourceNodeID, - edgeId: null, - nodeState: sourceState, - edgeState: false - }); + if (event.event === "STARTED") { + // Handle STARTED event logic if needed (parallel execution) + sourceState = "executing"; + targetState = "executing"; + edgeActivated = true; + + } else if (event.event === "STOPPED") { + // Handle STOPPED event logic if needed (external interruption, target is response kinda recovery) + sourceState = "failed"; + targetState = "executing"; + edgeActivated = true; + + } else if (event.event === "FAILED") { + // Handle FAILED event logic if needed (failure usually due to an error, target is recovery) + sourceState = "failed"; + targetState = "executing "; + edgeActivated = true; + + } else if (event.event === "SUCCEEDED") { + // Handle SUCCEEDED event logic if needed (target is sequential execution) + sourceState = "complete"; + targetState = "executing"; + edgeActivated = true; + + } else { + // Handle other events or default case + console.warn(`[GraphUpdater] Unknown event type for session ${sessionId}:`, event.event); + return; + } - eventId++; + graph.eventLog.push({ + eventId: eventId, + nodeId: sourceNodeID, + edgeId: null, + nodeState: sourceState, + edgeState: false + }); - graph.eventLog.push({ - eventId: eventId, - nodeId: targetNodeID, - edgeId: edgeID, - nodeState: targetState, - edgeState: edgeActivated - }); + eventId++; - await graph.save(); + graph.eventLog.push({ + eventId: eventId, + nodeId: targetNodeID, + edgeId: edgeID, + nodeState: targetState, + edgeState: edgeActivated + }); - return graph; - } + await graph.save(); + + return graph; } module.exports = { createGraphForSession, finalizeGraphForSession, updateGraphWithRunnerEvent, - updateGraphNumbers, - getActiveGraphKey, }; \ No newline at end of file diff --git a/backend/utils/graphQueueManager.js b/backend/utils/graphQueueManager.js index fa0b552..41696ab 100644 --- a/backend/utils/graphQueueManager.js +++ b/backend/utils/graphQueueManager.js @@ -1,11 +1,9 @@ const { createGraphForSession, finalizeGraphForSession, - updateGraphWithRunnerEvent, - updateGraphNumbers, - getActiveGraphKey, } = require("./graphManage") + updateGraphWithRunnerEvent,} = require("./graphManage") -const runnerQueues = new Map(); // sessionId => { define: [], event: [] } +const sessionQueues = new Map(); // sessionId => { define: [], event: [] } let broadcastGraphFn = null; /** Sets the function to broadcast graph updates. @@ -18,89 +16,119 @@ function setGraphBroadcast(fn) { broadcastGraphFn = fn; } -async function addToQueue(event, sessionId) { - const keyTest = getActiveGraphKey(sessionId); - - if (!keyTest) { - console.warn(`[QueueProcessor] No active graph key for session ${sessionId} so creating a new one.`); - await updateGraphNumbers(sessionId); +async function process(event, sessionId) { + // Validate the sessionId and event. + if (!sessionId) { + console.warn("[QueueProcessor] No session ID provided, cannot process event."); + return; } - const key = getActiveGraphKey(sessionId); - - if (!runnerQueues.has(key)) { - runnerQueues.set(key, { + // Initialize the session queue if it doesn't exist. + if (!sessionQueues.has(sessionId)) { + sessionQueues.set(sessionId, { + queue: [], define: [], - event: [], - processing: false + processing: false, + process_state: "IDLE" // IDLE, DEFINE, EVENT }); } + // Check if the event is valid. if (!event || typeof event.type !== "string") return; - const queues = runnerQueues.get(key); + // Retrieve the queue for the given session ID. + const queue = sessionQueues.get(sessionId); - if (event.type === "RUNNER_DEFINE") { - queues.define.push(event); - } else if (event.type === "RUNNER_EVENT") { - queues.event.push(event); + // Push the event into the queue based on if it is a RUNNER_DEFINE or RUNNER_EVENT. + if (event.type === "RUNNER_DEFINE" || event.type === "RUNNER_EVENT") { + queue.queue.push(event); } await processQueues(sessionId); } async function processQueues(sessionId) { - const key = getActiveGraphKey(sessionId); + // Retrieve the queues for the given session ID. + const queue = sessionQueues.get(sessionId); - const queues = runnerQueues.get(key); - if (!queues || queues.processing) return; + // If the queue is already processing, return early to avoid re-entrancy issues. + if (!queue || queue.processing) return; - queues.processing = true; + // Set the queue to processing state. + queue.processing = true; - try { - if (queues.define.length > 0 && queues.event.length === 1) { - const newGraph = await createGraphForSession(sessionId, queues.define); + const eventsToProcess = [...queue.queue]; // clone current queue + queue.queue.length = 0; // clear early to avoid mid-loop appends - if (broadcastGraphFn && newGraph) { - broadcastGraphFn({ type: "GRAPH_UPDATE", graph: newGraph }); - } + for (const element of eventsToProcess) { + try { + if (queue.process_state === "IDLE" && element.type === "RUNNER_DEFINE") { + queue.process_state = "DEFINE"; + console.info(`[QueueProcessor] Starting DEFINE state for session ${sessionId}`); - queues.define = []; - } + // If we are coming from IDLE state and get RUNNER_DEFINE, push the event into define queue + queue.define.push(element); - while (queues.event.length > 0) { - const evt = queues.event.shift(); + } else if (queue.process_state === "DEFINE" && element.type === "RUNNER_DEFINE") { + queue.process_state = "DEFINE"; + console.info(`[QueueProcessor] Continuing DEFINE state for session ${sessionId}`); - const updatedGraph = await updateGraphWithRunnerEvent(sessionId, evt); + // If we are in DEFINE and still getting RUNNER_DEFINE, push the event into define queue + queue.define.push(element); - if (broadcastGraphFn && updatedGraph) { - broadcastGraphFn({ type: "GRAPH_UPDATE", graph: updatedGraph }); - } - } + } else if (queue.process_state === "DEFINE" && element.type === "RUNNER_EVENT") { + queue.process_state = "EVENT"; + console.info(`[QueueProcessor] Transitioning to EVENT state for session ${sessionId}`); + + // If we are in DEFINE state and RUNNER_EVENT are starting, create the graph with define queue + const newGraph = await createGraphForSession(sessionId, queue.define); - if (queues.define.length === 1 && queues.event.length === 0) { - await finalizeGraphForSession(sessionId); + if (broadcastGraphFn && newGraph) { + broadcastGraphFn({ type: "GRAPH_UPDATE", graph: newGraph }); + } - const newKey = getActiveGraphKey(sessionId); + // Empty the define queue after creating the graph + queue.define.length = 0; - runnerQueues.set(newKey, { - define: [], - event: [], - processing: false - }); + // update the graph with event + const updatedGraph = await updateGraphWithRunnerEvent(sessionId, element); - const newQueues = runnerQueues.get(newKey); + if (broadcastGraphFn && updatedGraph) { + broadcastGraphFn({ type: "GRAPH_UPDATE", graph: updatedGraph }); + } - newQueues.define = queues.define.slice(0, 1); + } else if (queue.process_state === "EVENT" && element.type === "RUNNER_EVENT") { + queue.process_state = "EVENT"; + console.info(`[QueueProcessor] Continuing EVENT state for session ${sessionId}`); + + // If we are in EVENT state and getting RUNNER_EVENT, update the graph with the event // update the graph with event + const updatedGraph = await updateGraphWithRunnerEvent(sessionId, element); + + if (broadcastGraphFn && updatedGraph) { + broadcastGraphFn({ type: "GRAPH_UPDATE", graph: updatedGraph }); + } + + } else if (queue.process_state === "EVENT" && element.type === "RUNNER_DEFINE") { + queue.process_state = "DEFINE"; + console.info(`[QueueProcessor] Transitioning to DEFINE state from EVENT for session ${sessionId}`); + + // If we are in EVENT state and getting RUNNER_DEFINE, finalize the graph for the session + await finalizeGraphForSession(sessionId); + + // Add the RUNNER_DEFINE to the define queue + queue.define.push(element); + } + } catch (err) { + console.error(`[QueueProcessor] ❌ Error processing element: ${err.message}`); + queue.processing = false; + return; } - } catch (err) { - console.error(`[QueueProcessor] ❌ Error: ${err.message}`); - } finally { - queues.processing = false; } + + queue.processing = false; } module.exports = { - addToQueue, + process, setGraphBroadcast, }; \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..db1fb83 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,49 @@ +services: + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: event_frontend + network_mode: host + restart: unless-stopped + + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: event_backend + environment: + - PORT=5000 + - MONGO_URI=mongodb://localhost:27017/event_logger + - ROSBRIDGE_URL=ws://localhost:8765 + network_mode: host + restart: unless-stopped + + mongo: + image: mongo:6 + container_name: mongo_db + network_mode: host + restart: unless-stopped + volumes: + - mongo_data:/data/db + attach: false + + rosbridge: + build: + context: ./ros2-client + dockerfile: Dockerfile + container_name: foxglove_bridge + network_mode: host + restart: unless-stopped + environment: + - RMW_IMPLEMENTATION=rmw_cyclonedds_cpp + - ROS_DOMAIN_ID=0 + - port=8765 + - address=0.0.0.0 + - topic_whitelist=['/events'] + - tls=false + attach: false + + +volumes: + mongo_data: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..2203164 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,11 @@ +node_modules +build +dist +npm-debug.log +Dockerfile +.dockerignore +.git +.gitignore +.env +*.md +*.log \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..6730969 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,36 @@ +# Step 1: Build the React app +FROM node:18-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy only package files first for caching +COPY package.json package-lock.json ./ + +# Install dependencies +RUN npm ci + +# Copy all other source files +COPY . . + +# Build the app +RUN npm run build + +# Step 2: Serve the build using Nginx +FROM nginx:alpine + +# Remove default nginx static assets +RUN rm -rf /usr/share/nginx/html/* + +# Copy built React app to Nginx static folder +COPY --from=builder /app/build /usr/share/nginx/html + +# Use custom config +RUN rm /etc/nginx/conf.d/default.conf +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose default HTTP port +EXPOSE 3000 + +# Run Nginx in the foreground +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..31b3579 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,16 @@ +# frontend/nginx.conf + +server { + listen 3000; # <-- Make Nginx listen on port 3000 + + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri /index.html; + } + + error_page 404 /index.html; +} diff --git a/frontend/public/index.html b/frontend/public/index.html index cb3c5e8..8dad2b9 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - Capabilities2 UI + Event Logger UI diff --git a/frontend/src/components/GraphTimelinePlayer.js b/frontend/src/components/GraphTimelinePlayer.js index 06acfa2..f18bb8c 100644 --- a/frontend/src/components/GraphTimelinePlayer.js +++ b/frontend/src/components/GraphTimelinePlayer.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import GraphCanvas from "./GraphCanvas"; import "./GraphTimelinePlayer.css"; @@ -11,6 +11,18 @@ function GraphTimelinePlayer({ graph }) { setStep(0); }, [graph]); + const nodeLookup = useMemo(() => { + const map = new Map(); + graph?.nodes?.forEach((n) => map.set(n.nodeId, n)); + return map; + }, [graph]); + + const edgeLookup = useMemo(() => { + const map = new Map(); + graph?.edges?.forEach((e) => map.set(e.edgeId, e)); + return map; + }, [graph]); + useEffect(() => { if (!graph) return; @@ -19,7 +31,6 @@ function GraphTimelinePlayer({ graph }) { const eventLog = graph.eventLog || []; - // If no timeline, just show idle nodes and basic edges if (eventLog.length === 0) { const idleNodes = graph.nodes.map((n) => ({ ...n, @@ -42,11 +53,11 @@ function GraphTimelinePlayer({ graph }) { for (let i = 0; i <= step && i < eventLog.length; i++) { const entry = eventLog[i]; - // Handle Node State + // Node if (entry.nodeId !== null) { let node = nodeMap.get(entry.nodeId); if (!node) { - const orig = graph.nodes.find((n) => n.nodeId === entry.nodeId); + const orig = nodeLookup.get(entry.nodeId); if (orig) { node = { ...orig, @@ -60,11 +71,11 @@ function GraphTimelinePlayer({ graph }) { } } - // Handle Edge State + // Edge if (entry.edgeId !== null) { let edge = edgeMap.get(entry.edgeId); if (!edge) { - const orig = graph.edges.find((e) => e.edgeId === entry.edgeId); + const orig = edgeLookup.get(entry.edgeId); if (orig) { edge = { ...orig, @@ -74,28 +85,32 @@ function GraphTimelinePlayer({ graph }) { }; edgeMap.set(entry.edgeId, edge); } - } else if (entry.edgeState) { - edge.activated += 1; + } else { + edge.activated = entry.edgeState ? 1 : 0; } } } setCurrentNodes([...nodeMap.values()]); setCurrentEdges([...edgeMap.values()]); - }, [graph, step]); + }, [graph, step, nodeLookup, edgeLookup]); if (!graph) return

No graph loaded.

; + const totalSteps = graph.eventLog?.length || 0; + return (
- + - Step {graph.eventLog.length ? step + 1 : 0} / {graph.eventLog.length} + Step {totalSteps ? step + 1 : 0} / {totalSteps}