From 99ed39df0789a2082d1f6a15d55664713b05620e Mon Sep 17 00:00:00 2001 From: ilia gurielidze Date: Fri, 16 May 2025 18:17:54 +0400 Subject: [PATCH] first commit --- .cursorignore | 1 + .env | 3 + .gitignore | 1 + AutoCadTrack.cs | 1649 +++++++++++++++++++++ README.md | 50 + controllers/autocadController.js | 33 + data/events_2025-05-16T12-44-37.278Z.json | 1225 +++++++++++++++ models/AutocadEvent.js | 39 + package-lock.json | 1594 ++++++++++++++++++++ package.json | 28 + public/css/style.css | 225 +++ public/index.html | 103 ++ public/js/app.js | 532 +++++++ routes/autocadRoutes.js | 11 + server.js | 712 +++++++++ test-client.js | 63 + 16 files changed, 6269 insertions(+) create mode 100644 .cursorignore create mode 100644 .env create mode 100644 .gitignore create mode 100644 AutoCadTrack.cs create mode 100644 README.md create mode 100644 controllers/autocadController.js create mode 100644 data/events_2025-05-16T12-44-37.278Z.json create mode 100644 models/AutocadEvent.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/css/style.css create mode 100644 public/index.html create mode 100644 public/js/app.js create mode 100644 routes/autocadRoutes.js create mode 100644 server.js create mode 100644 test-client.js diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/.cursorignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..e48f357 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +PORT=3000 +# Uncomment and set this if you want to use MongoDB +# MONGODB_URI=mongodb://localhost:27017/autocad_tracker \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/AutoCadTrack.cs b/AutoCadTrack.cs new file mode 100644 index 0000000..f337592 --- /dev/null +++ b/AutoCadTrack.cs @@ -0,0 +1,1649 @@ +using Autodesk.AutoCAD.ApplicationServices; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.EditorInput; +using Autodesk.AutoCAD.Runtime; +using Autodesk.AutoCAD.Geometry; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Linq; +using System.Timers; + +// Register command class with AutoCAD +[assembly: CommandClass(typeof(AutoCADChangeTracker.Commands))] + +namespace AutoCADChangeTracker +{ + /// + /// Main command class for the AutoCAD Change Tracker + /// + public class Commands + { + // Store original positions of objects + private static Dictionary originalPositions = new Dictionary(); + + // Store original transformation matrices for blocks + private static Dictionary originalBlockTransforms = new Dictionary(); + + // Store original attribute values for blocks + private static Dictionary> originalBlockAttributes = new Dictionary>(); + + // Track whether monitoring is active + private static bool isTracking = false; + + // Timer for periodic scans + private static System.Timers.Timer scanTimer = null; + + // Queue for network operations to avoid blocking the main thread + private static Queue eventQueue = new Queue(); + private static object queueLock = new object(); + + // Background thread for sending events + private static System.Threading.Thread senderThread = null; + private static bool keepSenderRunning = false; + + // Performance monitoring + private static DateTime lastPerformanceCheck = DateTime.MinValue; + private static int operationsCount = 0; + private static int maxOperationsPerMinute = 300; // Can be adjusted based on performance needs + + // Configurable settings + private static int scanIntervalMs = 5000; // 5 seconds default + private static int maxQueueSize = 1000; // Maximum events to queue + + // Server URL to send change data to (customize this) + private static string serverUrl = "http://localhost:3000/api/events"; + + /// + /// Command to start monitoring changes + /// + [CommandMethod("TRACKSTART")] + public static void StartTracking() + { + try + { + // Check if already tracking + if (isTracking) + { + LogMessage("Change tracking is already active."); + return; + } + + // Get the active document + Document doc = Application.DocumentManager.MdiActiveDocument; + if (doc == null) + { + LogMessage("No active document found."); + return; + } + + // Access the database + Database db = doc.Database; + + // Subscribe to events + db.ObjectAppended += OnObjectAppended; + db.ObjectModified += OnObjectModified; + db.ObjectErased += OnObjectErased; + + // Subscribe to document events to detect attribute editing + doc.CommandEnded += OnCommandEnded; + doc.CommandWillStart += OnCommandWillStart; + doc.CommandCancelled += OnCommandCancelled; + + // Setup periodic scanning timer with configurable interval + scanTimer = new System.Timers.Timer(scanIntervalMs); + scanTimer.Elapsed += (sender, e) => + { + // Skip scan if we're performing too many operations + if (IsOperationRateTooHigh()) + { + LogMessage("Skipping scan due to high operation rate."); + return; + } + + ScanForAttributeChanges(); + }; + scanTimer.AutoReset = true; + scanTimer.Enabled = true; + + // Initialize current state of blocks + CaptureCurrentBlockState(); + + // Start background sender thread + StartSenderThread(); + + // Set tracking flag + isTracking = true; + + // Reset performance counters + ResetPerformanceCounters(); + + // Notify user + LogMessage("AutoCAD Change Tracker started successfully."); + } + catch (global::System.Exception ex) + { + LogMessage($"Error starting change tracker: {ex.Message}"); + } + } + + /// + /// Command to stop monitoring changes + /// + [CommandMethod("TRACKSTOP")] + public static void StopTracking() + { + try + { + // Check if tracking is active + if (!isTracking) + { + LogMessage("Change tracking is not currently active."); + return; + } + + // Get the active document + Document doc = Application.DocumentManager.MdiActiveDocument; + if (doc == null) + { + LogMessage("No active document found."); + return; + } + + // Access the database + Database db = doc.Database; + + // Unsubscribe from events - do this first to prevent new events + db.ObjectAppended -= OnObjectAppended; + db.ObjectModified -= OnObjectModified; + db.ObjectErased -= OnObjectErased; + + // Unsubscribe from document events + doc.CommandEnded -= OnCommandEnded; + doc.CommandWillStart -= OnCommandWillStart; + doc.CommandCancelled -= OnCommandCancelled; + + // Stop timer + if (scanTimer != null) + { + scanTimer.Stop(); + scanTimer.Dispose(); + scanTimer = null; + } + + // Stop sender thread + StopSenderThread(); + + // Clear tracking flag + isTracking = false; + + // Clean up tracking dictionaries to free memory + CleanupTrackingData(); + + // Notify user + LogMessage("AutoCAD Change Tracker stopped."); + } + catch (global::System.Exception ex) + { + LogMessage($"Error stopping change tracker: {ex.Message}"); + } + } + + /// + /// New command to adjust scan interval + /// + [CommandMethod("TRACKINTERVAL")] + public static void SetScanInterval() + { + try + { + Document doc = Application.DocumentManager.MdiActiveDocument; + if (doc == null) return; + + Editor ed = doc.Editor; + + // Prompt for new interval + PromptIntegerOptions opts = new PromptIntegerOptions("\nEnter new scan interval in milliseconds (min 1000):"); + opts.AllowNegative = false; + opts.AllowZero = false; + opts.DefaultValue = scanIntervalMs; + opts.UseDefaultValue = true; + + PromptIntegerResult result = ed.GetInteger(opts); + if (result.Status != PromptStatus.OK) return; + + // Validate and set new interval + int newInterval = Math.Max(1000, result.Value); // Minimum 1 second + scanIntervalMs = newInterval; + + // Update active timer if tracking is on + if (isTracking && scanTimer != null) + { + scanTimer.Interval = scanIntervalMs; + } + + LogMessage($"Scan interval updated to {scanIntervalMs} ms."); + } + catch (global::System.Exception ex) + { + LogMessage($"Error setting scan interval: {ex.Message}"); + } + } + + /// + /// Cleanup tracking data to free memory + /// + private static void CleanupTrackingData() + { + // Clear all tracking dictionaries + originalPositions.Clear(); + originalBlockTransforms.Clear(); + originalBlockAttributes.Clear(); + + // Force garbage collection + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + + /// + /// Reset performance counters + /// + private static void ResetPerformanceCounters() + { + lastPerformanceCheck = DateTime.Now; + operationsCount = 0; + } + + /// + /// Check if operation rate is too high to perform additional operations + /// + private static bool IsOperationRateTooHigh() + { + // Increment operation counter + operationsCount++; + + // Check if a minute has passed since last check + TimeSpan elapsed = DateTime.Now - lastPerformanceCheck; + if (elapsed.TotalMinutes >= 1) + { + // Reset counters if a minute has passed + ResetPerformanceCounters(); + return false; + } + + // If we've exceeded our operation rate limit, throttle + return operationsCount > maxOperationsPerMinute; + } + + /// + /// Start background thread for sending events to server + /// + private static void StartSenderThread() + { + if (senderThread != null) return; + + keepSenderRunning = true; + senderThread = new System.Threading.Thread(ProcessEventQueue) + { + IsBackground = true, + Name = "AutoCAD_EventSender" + }; + senderThread.Start(); + } + + /// + /// Stop background sender thread + /// + private static void StopSenderThread() + { + if (senderThread == null) return; + + keepSenderRunning = false; + + // Wait for thread to finish, but not indefinitely + if (!senderThread.Join(2000)) + { + try + { + senderThread.Abort(); // Last resort + } + catch + { + // Ignore abort errors + } + } + + senderThread = null; + } + + /// + /// Background thread method for processing event queue + /// + private static void ProcessEventQueue() + { + while (keepSenderRunning) + { + object eventData = null; + + // Safely dequeue an event + lock (queueLock) + { + if (eventQueue.Count > 0) + { + eventData = eventQueue.Dequeue(); + } + } + + // Process event if we have one + if (eventData != null) + { + try + { + SendEventToServer(eventData).Wait(); + } + catch (global::System.Exception ex) + { + // Log error but continue processing queue + LogMessage($"Error sending event: {ex.Message}"); + } + } + else + { + // No events to process, sleep to avoid spinning + System.Threading.Thread.Sleep(100); + } + } + } + + /// + /// Helper method to enqueue event for background sending + /// + private static void SendToServer(object data) + { + try + { + lock (queueLock) + { + // Limit queue size to prevent memory issues + if (eventQueue.Count > maxQueueSize) + { + // Remove oldest event if queue is full + eventQueue.Dequeue(); + } + + // Add the new event + eventQueue.Enqueue(data); + } + } + catch (global::System.Exception ex) + { + LogMessage($"Error queueing event: {ex.Message}"); + } + } + + /// + /// Async method to actually send data to server + /// + private static async Task SendEventToServer(object data) + { + try + { + // Create HTTP client + using var client = new HttpClient(); + client.Timeout = TimeSpan.FromSeconds(5); + + // Convert data to JSON + string json = JsonSerializer.Serialize(data); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + // Send data asynchronously + var response = await client.PostAsync(serverUrl, content); + + // We don't log success to reduce noise, only log failures + if (!response.IsSuccessStatusCode) + { + LogMessage($"Server returned error: {response.StatusCode}"); + } + } + catch (global::System.Exception ex) + { + LogMessage($"Error sending data to server: {ex.Message}"); + } + } + + /// + /// Event handler for command start + /// + private static void OnCommandWillStart(object sender, CommandEventArgs e) + { + try + { + // Skip if operation rate is too high + if (IsOperationRateTooHigh()) return; + + // Capture current state before command runs for comparison later + if (IsAttributeEditCommand(e.GlobalCommandName)) + { + LogMessage($"Attribute edit command starting: {e.GlobalCommandName}"); + // Take a snapshot to compare later + CaptureCurrentBlockState(); + } + } + catch (global::System.Exception ex) + { + LogMessage($"Error in command will start handler: {ex.Message}"); + } + } + + /// + /// Event handler for command completion - used to detect attribute edits + /// + private static void OnCommandEnded(object sender, CommandEventArgs e) + { + try + { + // Skip if operation rate is too high + if (IsOperationRateTooHigh()) return; + + // Check for attribute editing commands + if (IsAttributeEditCommand(e.GlobalCommandName)) + { + LogMessage($"Attribute edit command ended: {e.GlobalCommandName}"); + + // Scan for attribute changes after attribute editing commands + // But delay slightly to let AutoCAD complete its operations + Application.Idle += DelayedAttributeScan; + } + } + catch (global::System.Exception ex) + { + LogMessage($"Error in command ended handler: {ex.Message}"); + } + } + + /// + /// Helper for delayed attribute scanning + /// + private static void DelayedAttributeScan(object sender, EventArgs e) + { + // Remove handler after first execution + Application.Idle -= DelayedAttributeScan; + + try + { + // Wait a brief moment to let AutoCAD finish processing + System.Threading.Thread.Sleep(100); + + // Do the actual scan + ScanForAttributeChanges(); + } + catch (global::System.Exception ex) + { + LogMessage($"Error in delayed attribute scan: {ex.Message}"); + } + } + + /// + /// Event handler for command cancellation + /// + private static void OnCommandCancelled(object sender, CommandEventArgs e) + { + // Skip if operation rate is too high + if (IsOperationRateTooHigh()) return; + + // Still check for changes even if command was cancelled + if (IsAttributeEditCommand(e.GlobalCommandName)) + { + LogMessage($"Attribute edit command cancelled: {e.GlobalCommandName}"); + + // Delay scan to prevent AutoCAD freezes + Application.Idle += DelayedAttributeScan; + } + } + + /// + /// Helper to check if a command is an attribute editing command + /// + private static bool IsAttributeEditCommand(string commandName) + { + string upperCmd = commandName.ToUpper(); + return upperCmd == "ATTEDIT" || + upperCmd == "EATTEDIT" || + upperCmd == "BATTMAN" || + upperCmd == "PROPERTIES" || + upperCmd == "DDEDIT" || + upperCmd == "ATTSYNC" || + upperCmd == "ATTIPEDIT" || + upperCmd == "ATTREDEF" || + upperCmd == "REFEDIT" || + upperCmd == "MLEDIT" || + upperCmd == "TEXTEDIT" || + upperCmd == "MODIFY" || + upperCmd == "CHANGE"; + } + + /// + /// Scans all blocks to check for attribute changes + /// + private static void ScanForAttributeChanges() + { + try + { + // Skip if operation rate is too high + if (IsOperationRateTooHigh()) return; + + // Make sure we're on the main thread + Autodesk.AutoCAD.ApplicationServices.Core.Application.Idle += new EventHandler(delegate(object s, EventArgs e) + { + Autodesk.AutoCAD.ApplicationServices.Core.Application.Idle -= new EventHandler(delegate(object s2, EventArgs e2) { }); + + try + { + Document doc = Application.DocumentManager.MdiActiveDocument; + if (doc == null) return; + + using (var tr = doc.TransactionManager.StartTransaction()) + { + // Get the current database + Database db = doc.Database; + + // Get the block table + BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable; + if (bt == null) + { + tr.Commit(); + return; + } + + // Get the model space block table record + BlockTableRecord modelSpace = tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead) as BlockTableRecord; + if (modelSpace == null) + { + tr.Commit(); + return; + } + + // Iterate through entities in model space - limit to 100 per scan to avoid freezes + int processedEntities = 0; + int maxEntitiesToProcess = 100; + + foreach (ObjectId objId in modelSpace) + { + // Limit processing to avoid freezes + if (processedEntities > maxEntitiesToProcess) break; + processedEntities++; + + // Skip if we don't have original data for this object + if (!originalBlockAttributes.ContainsKey(objId)) continue; + + Entity ent = tr.GetObject(objId, OpenMode.ForRead) as Entity; + if (ent == null) continue; + + // Check if this is a block reference + if (ent is BlockReference blockRef) + { + // Get the original attributes for this block + Dictionary oldAttributes = originalBlockAttributes[objId]; + + // Get the current attributes + Dictionary newAttributes = new Dictionary(); + Dictionary attributeChanges = new Dictionary(); + + // Collect current attribute values + foreach (ObjectId attId in blockRef.AttributeCollection) + { + if (tr.GetObject(attId, OpenMode.ForRead) is AttributeReference attRef) + { + // Store new attribute value + newAttributes[attRef.Tag] = attRef.TextString; + + // Check if attribute changed + if (oldAttributes.TryGetValue(attRef.Tag, out string oldValue) && oldValue != attRef.TextString) + { + attributeChanges[attRef.Tag] = new + { + OldValue = oldValue, + NewValue = attRef.TextString + }; + } + } + } + + // If we found changes, report them + if (attributeChanges.Count > 0) + { + LogMessage($"Detected attribute changes in block {blockRef.Name}"); + + // Get current position + Point3d position = GetEntityPosition(blockRef); + + // Create event data for attribute changes + var blockData = new + { + EventType = "BlockAttributesChanged", + ObjectId = blockRef.ObjectId.Handle.ToString(), + BlockName = blockRef.Name, + Position = new { X = position.X, Y = position.Y, Z = position.Z }, + AttributeChanges = attributeChanges, + Attributes = newAttributes, + Timestamp = DateTime.UtcNow + }; + + // Send event to server + SendToServer(blockData); + + // Update our stored attributes + originalBlockAttributes[objId] = newAttributes; + } + } + } + + tr.Commit(); + } + } + catch (global::System.Exception ex) + { + LogMessage($"Error during periodic attribute scan: {ex.Message}"); + } + }); + } + catch (global::System.Exception ex) + { + LogMessage($"Error scanning for attribute changes: {ex.Message}"); + } + } + + /// + /// Captures the current state of all blocks in the drawing + /// + private static void CaptureCurrentBlockState() + { + try + { + Document doc = Application.DocumentManager.MdiActiveDocument; + if (doc == null) return; + + using (var tr = doc.TransactionManager.StartTransaction()) + { + // Get the current database + Database db = doc.Database; + + // Get the block table + BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable; + if (bt == null) + { + tr.Commit(); + return; + } + + // Get the model space block table record + BlockTableRecord modelSpace = tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead) as BlockTableRecord; + if (modelSpace == null) + { + tr.Commit(); + return; + } + + // Count for performance monitoring + int processedCount = 0; + int maxToProcess = 500; // Limit to prevent freezing on huge drawings + + // Flag to check if this is the first capture + bool isFirstCapture = originalPositions.Count == 0; + + // Iterate through entities in model space + foreach (ObjectId objId in modelSpace) + { + // Limit processing for performance + processedCount++; + if (processedCount > maxToProcess && !isFirstCapture) break; + + // Skip already tracked entities on subsequent captures + if (!isFirstCapture && originalPositions.ContainsKey(objId)) continue; + + Entity ent = tr.GetObject(objId, OpenMode.ForRead) as Entity; + if (ent == null) continue; + + // Store position for all entities + Point3d position = GetEntityPosition(ent); + originalPositions[ent.ObjectId] = position; + + // For block references, store additional information + if (ent is BlockReference blockRef) + { + // Store transformation matrix + originalBlockTransforms[blockRef.ObjectId] = blockRef.BlockTransform; + + // Store attribute values + Dictionary attributes = new Dictionary(); + foreach (ObjectId attId in blockRef.AttributeCollection) + { + if (tr.GetObject(attId, OpenMode.ForRead) is AttributeReference attRef) + { + attributes[attRef.Tag] = attRef.TextString; + } + } + + originalBlockAttributes[blockRef.ObjectId] = attributes; + } + } + + tr.Commit(); + } + + // If it's the first capture and we have lots of entities, suggest lower frequency + if (originalPositions.Count > 1000 && scanIntervalMs < 10000) + { + LogMessage($"Large drawing detected ({originalPositions.Count} entities). Consider increasing scan interval with TRACKINTERVAL command."); + } + } + catch (global::System.Exception ex) + { + LogMessage($"Error capturing block state: {ex.Message}"); + } + } + + /// + /// Event handler for object additions + /// + private static void OnObjectAppended(object sender, ObjectEventArgs e) + { + try + { + // Check if object is an entity + if (e.DBObject is not Entity entity) + { + return; + } + + // Access the entity safely within a transaction + Document doc = Application.DocumentManager.MdiActiveDocument; + using (var tr = doc.TransactionManager.StartTransaction()) + { + // Open the entity for read + entity = (Entity)tr.GetObject(entity.ObjectId, OpenMode.ForRead); + + // Get position + Point3d position = GetEntityPosition(entity); + + // Store position + originalPositions[entity.ObjectId] = position; + + // If it's a block reference, store additional information + if (entity is BlockReference blockRef) + { + // Store transformation matrix + originalBlockTransforms[blockRef.ObjectId] = blockRef.BlockTransform; + + // Store attribute values + Dictionary attributes = new Dictionary(); + foreach (ObjectId attId in blockRef.AttributeCollection) + { + if (tr.GetObject(attId, OpenMode.ForRead) is AttributeReference attRef) + { + attributes[attRef.Tag] = attRef.TextString; + } + } + + originalBlockAttributes[blockRef.ObjectId] = attributes; + + // Log event with block name + LogMessage($"Block added: {blockRef.Name} at ({position.X:F2}, {position.Y:F2}, {position.Z:F2})"); + + // Send block-specific event data to server + var blockData = new + { + EventType = "BlockAdded", + ObjectId = blockRef.ObjectId.Handle.ToString(), + BlockName = blockRef.Name, + Position = new { X = position.X, Y = position.Y, Z = position.Z }, + Scale = new { X = blockRef.ScaleFactors.X, Y = blockRef.ScaleFactors.Y, Z = blockRef.ScaleFactors.Z }, + Rotation = blockRef.Rotation, + Attributes = attributes, + Timestamp = DateTime.UtcNow + }; + + SendToServer(blockData); + } + else + { + // Log event for non-block entities + LogMessage($"Object added: {entity.GetType().Name} at ({position.X:F2}, {position.Y:F2}, {position.Z:F2})"); + + // Send event data to server + var data = new + { + EventType = "Added", + ObjectId = entity.ObjectId.Handle.ToString(), + ObjectType = entity.GetType().Name, + Position = new { X = position.X, Y = position.Y, Z = position.Z }, + Timestamp = DateTime.UtcNow + }; + + SendToServer(data); + } + + // Commit transaction + tr.Commit(); + } + } + catch (global::System.Exception ex) + { + LogMessage($"Error tracking new object: {ex.Message}"); + } + } + + /// + /// Event handler for object erasure + /// + private static void OnObjectErased(object sender, ObjectErasedEventArgs e) + { + try + { + // Check if object is an entity + if (e.DBObject is not Entity entity) + { + return; + } + + // If we have been tracking this object + if (originalPositions.ContainsKey(entity.ObjectId)) + { + // Get the last known position + Point3d lastPosition = originalPositions[entity.ObjectId]; + + // Remove from tracking + originalPositions.Remove(entity.ObjectId); + + // Special handling for blocks + if (entity is BlockReference blockRef) + { + // Get block information + string blockName = blockRef.Name; + + // Get attribute data if we have it + Dictionary attributes = new Dictionary(); + if (originalBlockAttributes.ContainsKey(blockRef.ObjectId)) + { + attributes = originalBlockAttributes[blockRef.ObjectId]; + originalBlockAttributes.Remove(blockRef.ObjectId); + } + + // Remove from block transform tracking + originalBlockTransforms.Remove(blockRef.ObjectId); + + // Log event + LogMessage($"Block erased: {blockName} from ({lastPosition.X:F2}, {lastPosition.Y:F2}, {lastPosition.Z:F2})"); + + // Send block-specific event data + var blockData = new + { + EventType = "BlockErased", + ObjectId = blockRef.ObjectId.Handle.ToString(), + BlockName = blockName, + LastPosition = new { X = lastPosition.X, Y = lastPosition.Y, Z = lastPosition.Z }, + Attributes = attributes, + Timestamp = DateTime.UtcNow + }; + + SendToServer(blockData); + } + else + { + // Log event for non-block entities + LogMessage($"Object erased: {entity.GetType().Name} from ({lastPosition.X:F2}, {lastPosition.Y:F2}, {lastPosition.Z:F2})"); + + // Send event data to server + var data = new + { + EventType = "Erased", + ObjectId = entity.ObjectId.Handle.ToString(), + ObjectType = entity.GetType().Name, + LastPosition = new { X = lastPosition.X, Y = lastPosition.Y, Z = lastPosition.Z }, + Timestamp = DateTime.UtcNow + }; + + SendToServer(data); + } + } + } + catch (global::System.Exception ex) + { + LogMessage($"Error tracking erased object: {ex.Message}"); + } + } + + /// + /// Event handler for detecting modifications including stretch operations + /// + private static void OnObjectModified(object sender, ObjectEventArgs e) + { + try + { + // Skip if operation rate is too high + if (IsOperationRateTooHigh()) + { + return; + } + + // Check if object is an entity and has been tracked + if (e.DBObject is not Entity entity || !originalPositions.ContainsKey(entity.ObjectId)) + { + return; + } + + // Get the active document + Document? doc = Application.DocumentManager.MdiActiveDocument; + if (doc == null) return; + + using (var tr = doc.TransactionManager.StartTransaction()) + { + // Open the entity for read + if (tr.GetObject(entity.ObjectId, OpenMode.ForRead) is not Entity openEntity) + { + tr.Commit(); + return; + } + + // Get original position data + Point3d oldPosition = originalPositions[entity.ObjectId]; + Point3d newPosition = GetEntityPosition(openEntity); + + // Skip if position hasn't changed significantly (within tolerance) + if (oldPosition.DistanceTo(newPosition) < 0.001) + { + tr.Commit(); + return; + } + + // Special handling for blocks + if (openEntity is BlockReference blockRef) + { + HandleBlockModification(blockRef, tr, oldPosition, newPosition); + } + else + { + // Check if this is a stretch operation by examining command history and geometry changes + bool isStretch = false; + string lastCommand = string.Empty; + + try + { + // Try to get current command + string currentCommand = string.Empty; + + // Try to use CommandInProgress if available + if (doc.CommandInProgress != null) + { + currentCommand = doc.CommandInProgress; + } + // Option 2: Alternative - check current command through Editor + else if (doc.Editor != null) + { + var cmdMethodInfo = doc.Editor.GetType().GetMethod("GetCurrentCommand"); + if (cmdMethodInfo != null) + { + var result = cmdMethodInfo.Invoke(doc.Editor, null); + if (result != null) + { + currentCommand = result.ToString(); + } + } + } + + // Detect stretch-like operations + if (currentCommand.Contains("STRETCH") || + currentCommand.Contains("GRIPS") || + IsMostLikelyStretch(openEntity, oldPosition, newPosition)) + { + isStretch = true; + } + } + catch (global::System.Exception ex) + { + // Log the error but continue + LogMessage($"Error detecting stretch: {ex.Message}"); + + // Fall back to geometry-based detection only + isStretch = IsMostLikelyStretch(openEntity, oldPosition, newPosition); + } + + // Log position change + if (isStretch) + { + LogMessage($"Object STRETCHED: {openEntity.GetType().Name} from " + + $"({oldPosition.X:F2}, {oldPosition.Y:F2}, {oldPosition.Z:F2}) to " + + $"({newPosition.X:F2}, {newPosition.Y:F2}, {newPosition.Z:F2})"); + + // Send stretch event data to server + var data = new + { + EventType = "Stretched", + ObjectId = openEntity.ObjectId.Handle.ToString(), + ObjectType = openEntity.GetType().Name, + OldPosition = new { X = oldPosition.X, Y = oldPosition.Y, Z = oldPosition.Z }, + NewPosition = new { X = newPosition.X, Y = newPosition.Y, Z = newPosition.Z }, + Command = lastCommand, + Timestamp = DateTime.UtcNow + }; + + SendToServer(data); + } + else + { + // Regular modification (non-stretch) + LogMessage($"Object moved: {openEntity.GetType().Name} from " + + $"({oldPosition.X:F2}, {oldPosition.Y:F2}, {oldPosition.Z:F2}) to " + + $"({newPosition.X:F2}, {newPosition.Y:F2}, {newPosition.Z:F2})"); + + // Send regular modification data + var data = new + { + EventType = "Modified", + ObjectId = openEntity.ObjectId.Handle.ToString(), + ObjectType = openEntity.GetType().Name, + OldPosition = new { X = oldPosition.X, Y = oldPosition.Y, Z = oldPosition.Z }, + NewPosition = new { X = newPosition.X, Y = newPosition.Y, Z = newPosition.Z }, + Timestamp = DateTime.UtcNow + }; + + SendToServer(data); + } + + // Update stored position + originalPositions[entity.ObjectId] = newPosition; + } + + // Commit transaction + tr.Commit(); + } + } + catch (global::System.Exception ex) + { + LogMessage($"Error tracking modified object: {ex.Message}"); + } + } + + /// + /// Handles block modifications including attribute changes and transformations + /// + private static void HandleBlockModification(BlockReference blockRef, Transaction tr, Point3d oldPosition, Point3d newPosition) + { + try + { + // Get block name + string blockName = blockRef.Name; + + // Check if we have transform data for this block + bool hasTransformData = originalBlockTransforms.TryGetValue(blockRef.ObjectId, out Matrix3d oldTransform); + Matrix3d newTransform = blockRef.BlockTransform; + + // Determine if block was stretched, moved, rotated, or scaled + bool isStretched = false; + bool isRotated = false; + bool isScaled = false; + bool isMoved = !oldPosition.IsEqualTo(newPosition); + + // Extract scale and rotation from transforms + Point3d oldScale = hasTransformData ? new Point3d( + Math.Sqrt(oldTransform.CoordinateSystem3d.Xaxis.DotProduct(oldTransform.CoordinateSystem3d.Xaxis)), + Math.Sqrt(oldTransform.CoordinateSystem3d.Yaxis.DotProduct(oldTransform.CoordinateSystem3d.Yaxis)), + Math.Sqrt(oldTransform.CoordinateSystem3d.Zaxis.DotProduct(oldTransform.CoordinateSystem3d.Zaxis)) + ) : new Point3d(1, 1, 1); + + Point3d newScale = new Point3d( + Math.Sqrt(newTransform.CoordinateSystem3d.Xaxis.DotProduct(newTransform.CoordinateSystem3d.Xaxis)), + Math.Sqrt(newTransform.CoordinateSystem3d.Yaxis.DotProduct(newTransform.CoordinateSystem3d.Yaxis)), + Math.Sqrt(newTransform.CoordinateSystem3d.Zaxis.DotProduct(newTransform.CoordinateSystem3d.Zaxis)) + ); + + // Compare scale values + isScaled = Math.Abs(oldScale.X - newScale.X) > 0.001 || + Math.Abs(oldScale.Y - newScale.Y) > 0.001 || + Math.Abs(oldScale.Z - newScale.Z) > 0.001; + + // Extract rotation from transforms (simplified approach) + double oldRotation = hasTransformData ? Math.Atan2(oldTransform.CoordinateSystem3d.Xaxis.Y, oldTransform.CoordinateSystem3d.Xaxis.X) : blockRef.Rotation; + double newRotation = blockRef.Rotation; + + // Compare rotation + isRotated = Math.Abs(oldRotation - newRotation) > 0.001; + + // Check for stretch by examining if some parts moved while others remained fixed + isStretched = hasTransformData && isScaled && !isRotated && IsBlockStretched(blockRef, oldTransform, newTransform); + + // Get attribute changes + Dictionary oldAttributes = originalBlockAttributes.ContainsKey(blockRef.ObjectId) + ? originalBlockAttributes[blockRef.ObjectId] + : new Dictionary(); + + Dictionary newAttributes = new Dictionary(); + Dictionary attributeChanges = new Dictionary(); + + // Collect attribute data + foreach (ObjectId attId in blockRef.AttributeCollection) + { + if (tr.GetObject(attId, OpenMode.ForRead) is AttributeReference attRef) + { + // Store new attribute + newAttributes[attRef.Tag] = attRef.TextString; + + // Check if attribute changed + if (oldAttributes.TryGetValue(attRef.Tag, out string oldValue) && oldValue != attRef.TextString) + { + attributeChanges[attRef.Tag] = new + { + OldValue = oldValue, + NewValue = attRef.TextString + }; + } + } + } + + // Log the appropriate event type + string eventType; + if (isStretched) eventType = "BlockStretched"; + else if (isRotated) eventType = "BlockRotated"; + else if (isScaled) eventType = "BlockScaled"; + else if (isMoved) eventType = "BlockMoved"; + else if (attributeChanges.Count > 0) eventType = "BlockAttributesChanged"; + else eventType = "BlockModified"; + + // Log the event + LogMessage($"{eventType}: {blockName} from ({oldPosition.X:F2}, {oldPosition.Y:F2}, {oldPosition.Z:F2}) to ({newPosition.X:F2}, {newPosition.Y:F2}, {newPosition.Z:F2})"); + + // Create event data object + var blockData = new + { + EventType = eventType, + ObjectId = blockRef.ObjectId.Handle.ToString(), + BlockName = blockName, + OldPosition = new { X = oldPosition.X, Y = oldPosition.Y, Z = oldPosition.Z }, + NewPosition = new { X = newPosition.X, Y = newPosition.Y, Z = newPosition.Z }, + OldRotation = oldRotation, + NewRotation = newRotation, + OldScale = new { X = oldScale.X, Y = oldScale.Y, Z = oldScale.Z }, + NewScale = new { X = newScale.X, Y = newScale.Y, Z = newScale.Z }, + AttributeChanges = attributeChanges.Count > 0 ? attributeChanges : null, + Attributes = newAttributes, // Always include all current attributes + Timestamp = DateTime.UtcNow + }; + + // Send to server + SendToServer(blockData); + + // Update stored data + originalPositions[blockRef.ObjectId] = newPosition; + originalBlockTransforms[blockRef.ObjectId] = newTransform; + originalBlockAttributes[blockRef.ObjectId] = newAttributes; + } + catch (global::System.Exception ex) + { + LogMessage($"Error processing block modification: {ex.Message}"); + } + } + + /// + /// Determines if a block has been stretched (parts moved while others remained fixed) + /// + private static bool IsBlockStretched(BlockReference blockRef, Matrix3d oldTransform, Matrix3d newTransform) + { + try + { + // Get the inverse of the old transform + Matrix3d invOldTransform = oldTransform.Inverse(); + + // Compare non-uniform scaling as a sign of stretching + Point3d oldScale = new Point3d( + Math.Sqrt(oldTransform.CoordinateSystem3d.Xaxis.DotProduct(oldTransform.CoordinateSystem3d.Xaxis)), + Math.Sqrt(oldTransform.CoordinateSystem3d.Yaxis.DotProduct(oldTransform.CoordinateSystem3d.Yaxis)), + Math.Sqrt(oldTransform.CoordinateSystem3d.Zaxis.DotProduct(oldTransform.CoordinateSystem3d.Zaxis)) + ); + + Point3d newScale = new Point3d( + Math.Sqrt(newTransform.CoordinateSystem3d.Xaxis.DotProduct(newTransform.CoordinateSystem3d.Xaxis)), + Math.Sqrt(newTransform.CoordinateSystem3d.Yaxis.DotProduct(newTransform.CoordinateSystem3d.Yaxis)), + Math.Sqrt(newTransform.CoordinateSystem3d.Zaxis.DotProduct(newTransform.CoordinateSystem3d.Zaxis)) + ); + + // If the scales in X/Y/Z vary differently, that's a stretch + double scaleXDiff = Math.Abs(newScale.X / oldScale.X - 1.0); + double scaleYDiff = Math.Abs(newScale.Y / oldScale.Y - 1.0); + double scaleZDiff = Math.Abs(newScale.Z / oldScale.Z - 1.0); + + // If one dimension changed significantly more than others, it's likely a stretch + if (Math.Abs(scaleXDiff - scaleYDiff) > 0.01 || + Math.Abs(scaleXDiff - scaleZDiff) > 0.01 || + Math.Abs(scaleYDiff - scaleZDiff) > 0.01) + { + return true; + } + + return false; + } + catch + { + return false; + } + } + + /// + /// Helper method to determine if a modification is likely a stretch + /// + private static bool IsMostLikelyStretch(Entity entity, Point3d oldPos, Point3d newPos) + { + try + { + // For polylines, check if some vertices moved while others stayed fixed + if (entity is Polyline poly) + { + bool foundMovedVertex = false; + bool foundFixedVertex = false; + + // Sample a few vertices to see if some moved and some stayed fixed + for (int i = 0; i < poly.NumberOfVertices; i++) + { + Point3d oldVertexPos = poly.GetPoint3dAt(i); + + // Compare to original known positions (this would need enhancement for complete accuracy) + // This is a simple approximation that detects if some vertices moved but not all + double distance = oldPos.DistanceTo(oldVertexPos); + double newDistance = newPos.DistanceTo(oldVertexPos); + + if (Math.Abs(distance - newDistance) > 0.001) + { + foundMovedVertex = true; + } + else + { + foundFixedVertex = true; + } + + // If we found both moved and fixed vertices, it's likely a stretch + if (foundMovedVertex && foundFixedVertex) + { + return true; + } + } + } + + // For lines, we can check specific changes to endpoints + else if (entity is Line line) + { + // Get the endpoints + Point3d startPoint = line.StartPoint; + Point3d endPoint = line.EndPoint; + + // Calculate distances between old position and both endpoints + double oldStartDist = oldPos.DistanceTo(startPoint); + double newStartDist = newPos.DistanceTo(startPoint); + double oldEndDist = oldPos.DistanceTo(endPoint); + double newEndDist = newPos.DistanceTo(endPoint); + + // If one endpoint's distance changed significantly more than the other, + // it's likely one end was stretched while the other remained fixed + return Math.Abs(oldStartDist - newStartDist) > 0.1 && + Math.Abs(oldEndDist - newEndDist) < 0.01; + } + + // For other entity types, we'd need specific implementation + // Default to false for now + return false; + } + catch + { + // If anything goes wrong in our stretch detection, default to false + return false; + } + } + + /// + /// Helper method to get entity position + /// + private static Point3d GetEntityPosition(Entity entity) + { + // Handle different entity types + return entity switch + { + Line line => line.StartPoint, + Circle circle => circle.Center, + BlockReference blockRef => blockRef.Position, + _ => entity.Bounds.Value.MinPoint + }; + } + + /// + /// Helper method to write messages to the command line + /// + private static void LogMessage(string message) + { + try + { + Editor editor = Application.DocumentManager.MdiActiveDocument?.Editor; + editor?.WriteMessage($"\n{message}"); + } + catch + { + // Ignore logging errors + } + } + + /// + /// Command to manually clean up tracking data and improve performance + /// + [CommandMethod("TRACKCLEANUP")] + public static void CleanupTracking() + { + try + { + // Only proceed if tracking is active + if (!isTracking) + { + LogMessage("Change tracking is not active. No cleanup needed."); + return; + } + + // Get current memory usage before cleanup + long beforeMemory = GC.GetTotalMemory(false); + + // Temporarily suspend timer + if (scanTimer != null) + { + scanTimer.Enabled = false; + } + + // Count entries before cleanup + int positionsCount = originalPositions.Count; + int transformsCount = originalBlockTransforms.Count; + int attributesCount = originalBlockAttributes.Count; + + // Before removing, rebuild necessary tracking data + Document doc = Application.DocumentManager.MdiActiveDocument; + if (doc != null) + { + // Capture current state of all blocks as a clean baseline + LogMessage("Rebuilding tracking data from current document state..."); + CaptureCurrentBlockState(); + } + + // Force garbage collection + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + // Resume timer + if (scanTimer != null && isTracking) + { + scanTimer.Enabled = true; + } + + // Get memory after cleanup + long afterMemory = GC.GetTotalMemory(false); + + // Report results + LogMessage($"Tracking cleanup complete:"); + LogMessage($"Memory before: {beforeMemory / 1024} KB, after: {afterMemory / 1024} KB"); + LogMessage($"Memory freed: {(beforeMemory - afterMemory) / 1024} KB"); + LogMessage($"Objects being tracked: {originalPositions.Count} (was {positionsCount})"); + } + catch (global::System.Exception ex) + { + LogMessage($"Error during tracking cleanup: {ex.Message}"); + + // Make sure timer is re-enabled + if (scanTimer != null && isTracking) + { + scanTimer.Enabled = true; + } + } + } + + /// + /// Command to show tracking performance statistics + /// + [CommandMethod("TRACKSTATS")] + public static void ShowTrackingStats() + { + try + { + if (!isTracking) + { + LogMessage("Change tracking is not active. No statistics available."); + return; + } + + // Get current memory usage + long currentMemory = GC.GetTotalMemory(false); + + // Report statistics + LogMessage("AutoCAD Change Tracker Statistics:"); + LogMessage("-------------------------------"); + LogMessage($"Memory usage: {currentMemory / 1024} KB"); + LogMessage($"Scan interval: {scanIntervalMs} ms"); + LogMessage($"Max operations per minute: {maxOperationsPerMinute}"); + LogMessage($"Current operation count: {operationsCount}"); + LogMessage($"Time since last reset: {(DateTime.Now - lastPerformanceCheck).TotalMinutes:F1} minutes"); + LogMessage($"Event queue size: {eventQueue.Count} / {maxQueueSize}"); + LogMessage($"Objects tracked:"); + LogMessage($" - Positions: {originalPositions.Count}"); + LogMessage($" - Block transforms: {originalBlockTransforms.Count}"); + LogMessage($" - Block attributes: {originalBlockAttributes.Count}"); + + // If there's a significant mismatch in the dictionaries, suggest cleanup + if (Math.Abs(originalPositions.Count - originalBlockAttributes.Count) > 50) + { + LogMessage("Note: Tracking data seems inconsistent. Consider running TRACKCLEANUP."); + } + } + catch (global::System.Exception ex) + { + LogMessage($"Error showing tracking statistics: {ex.Message}"); + } + } + + /// + /// Command to adjust max operations per minute + /// + [CommandMethod("TRACKTHROTTLE")] + public static void SetOperationLimit() + { + try + { + Document doc = Application.DocumentManager.MdiActiveDocument; + if (doc == null) return; + + Editor ed = doc.Editor; + + // Prompt for new operation limit + PromptIntegerOptions opts = new PromptIntegerOptions("\nEnter max operations per minute (50-1000):"); + opts.AllowNegative = false; + opts.AllowZero = false; + opts.DefaultValue = maxOperationsPerMinute; + opts.UseDefaultValue = true; + + PromptIntegerResult result = ed.GetInteger(opts); + if (result.Status != PromptStatus.OK) return; + + // Validate and set new limit + int newLimit = Math.Max(50, Math.Min(1000, result.Value)); + maxOperationsPerMinute = newLimit; + + // Reset counters + ResetPerformanceCounters(); + + LogMessage($"Operation throttle limit updated to {maxOperationsPerMinute} operations per minute."); + } + catch (global::System.Exception ex) + { + LogMessage($"Error setting operation limit: {ex.Message}"); + } + } + + /// + /// Command to set server URL + /// + [CommandMethod("TRACKSERVER")] + public static void SetServerUrl() + { + try + { + Document doc = Application.DocumentManager.MdiActiveDocument; + if (doc == null) return; + + Editor ed = doc.Editor; + + // Prompt for new server URL + PromptStringOptions opts = new PromptStringOptions("\nEnter server URL:"); + opts.DefaultValue = serverUrl; + opts.UseDefaultValue = true; + + PromptResult result = ed.GetString(opts); + if (result.Status != PromptStatus.OK) return; + + string newUrl = result.StringResult.Trim(); + if (string.IsNullOrEmpty(newUrl)) + { + LogMessage("Server URL cannot be empty."); + return; + } + + // Update URL + serverUrl = newUrl; + LogMessage($"Server URL updated to: {serverUrl}"); + + // Test connection + TestServerConnection(); + } + catch (global::System.Exception ex) + { + LogMessage($"Error setting server URL: {ex.Message}"); + } + } + + /// + /// Test server connection + /// + private static async void TestServerConnection() + { + try + { + using var client = new HttpClient(); + client.Timeout = TimeSpan.FromSeconds(5); + + // Ping the server with a test message + var testData = new + { + EventType = "ConnectionTest", + Timestamp = DateTime.UtcNow + }; + + string json = JsonSerializer.Serialize(testData); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + LogMessage("Testing server connection..."); + var response = await client.PostAsync(serverUrl, content); + + if (response.IsSuccessStatusCode) + { + LogMessage("Server connection test successful."); + } + else + { + LogMessage($"Server connection test failed: {response.StatusCode}"); + } + } + catch (global::System.Exception ex) + { + LogMessage($"Server connection test failed: {ex.Message}"); + } + } + + /// + /// Command to view and modify all tracking settings in one place + /// + [CommandMethod("TRACKSETTINGS")] + public static void ManageSettings() + { + try + { + Document doc = Application.DocumentManager.MdiActiveDocument; + if (doc == null) return; + + Editor ed = doc.Editor; + + // Show current settings first + LogMessage("--- AutoCAD Tracker Settings ---"); + LogMessage($"Tracking status: {(isTracking ? "Active" : "Inactive")}"); + LogMessage($"Scan interval: {scanIntervalMs} ms"); + LogMessage($"Max operations per minute: {maxOperationsPerMinute}"); + LogMessage($"Server URL: {serverUrl}"); + LogMessage($"Event queue size: {eventQueue.Count}"); + LogMessage(""); + + // Prompt for what to modify + PromptKeywordOptions keyOpts = new PromptKeywordOptions("\nSelect setting to modify:"); + keyOpts.Keywords.Add("Interval"); + keyOpts.Keywords.Add("Operations"); + keyOpts.Keywords.Add("Server"); + keyOpts.Keywords.Add("Cleanup"); + keyOpts.Keywords.Add("Status"); + keyOpts.Keywords.Add("Cancel"); + keyOpts.Keywords.Default = "Cancel"; + + PromptResult keyRes = ed.GetKeywords(keyOpts); + if (keyRes.Status != PromptStatus.OK || keyRes.StringResult == "Cancel") + return; + + switch (keyRes.StringResult) + { + case "Interval": + SetScanInterval(); + break; + case "Operations": + SetOperationLimit(); + break; + case "Server": + SetServerUrl(); + break; + case "Cleanup": + CleanupTracking(); + break; + case "Status": + ShowTrackingStats(); + break; + } + } + catch (global::System.Exception ex) + { + LogMessage($"Error managing settings: {ex.Message}"); + } + } + + /// + /// Command to show help information about tracker commands + /// + [CommandMethod("TRACKHELP")] + public static void ShowHelp() + { + LogMessage("--- AutoCAD Change Tracker Commands ---"); + LogMessage("TRACKSTART - Start change tracking"); + LogMessage("TRACKSTOP - Stop change tracking"); + LogMessage("TRACKSETTINGS - View and modify tracking settings"); + LogMessage("TRACKINTERVAL - Set scan interval (milliseconds)"); + LogMessage("TRACKTHROTTLE - Set max operations per minute"); + LogMessage("TRACKSERVER - Set server URL"); + LogMessage("TRACKSTATS - Show tracking statistics"); + LogMessage("TRACKCLEANUP - Clean up tracking data to free memory"); + LogMessage("TRACKHELP - Show this help information"); + LogMessage(""); + LogMessage("For performance issues:"); + LogMessage("1. Increase scan interval (TRACKINTERVAL) to 10000 or higher"); + LogMessage("2. Reduce operations limit (TRACKTHROTTLE) to 100-200"); + LogMessage("3. Run TRACKCLEANUP periodically during long sessions"); + LogMessage("4. For large drawings, stop tracking when not needed"); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..76b6ea4 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# AutoCAD Server + +A simple Node.js server to receive and store events from an AutoCAD plugin. + +## Features + +- Receives object creation and modification events from AutoCAD +- Stores events in memory (with option to use MongoDB) +- Provides API endpoints to retrieve stored events +- Simple setup and configuration + +## Setup + +1. Clone this repository +2. Install dependencies: + ``` + npm install + ``` +3. Configure environment variables in `.env` file (optional) +4. Start the server: + ``` + node server.js + ``` + +## Usage + +The server provides the following endpoints: + +- `POST /api/autocad-events`: Endpoint for receiving events from AutoCAD +- `GET /api/autocad-events`: Retrieve all stored events + +## AutoCAD Plugin Configuration + +Update the `ServerEndpoint` variable in your AutoCAD plugin to point to this server: + +```csharp +private static readonly string ServerEndpoint = "http://localhost:3000/api/autocad-events"; +``` + +## Using with MongoDB (Optional) + +To store events in MongoDB instead of memory: + +1. Uncomment the MongoDB connection code in `server.js` +2. Set the `MONGODB_URI` in the `.env` file +3. Uncomment the MongoDB code in the controller + +## License + +ISC \ No newline at end of file diff --git a/controllers/autocadController.js b/controllers/autocadController.js new file mode 100644 index 0000000..a6ff9d2 --- /dev/null +++ b/controllers/autocadController.js @@ -0,0 +1,33 @@ +// Optional: If you want to use MongoDB later +// const AutocadEvent = require('../models/AutocadEvent'); + +// Handle incoming AutoCAD events +exports.receiveEvent = (req, res) => { + try { + const eventData = req.body; + + // Log received data + console.log('Received AutoCAD event:', JSON.stringify(eventData, null, 2)); + + // Store in our simple in-memory storage + global.autocadEvents.push({ + ...eventData, + receivedAt: new Date() + }); + + // Optional: Store in MongoDB + // const event = new AutocadEvent(eventData); + // await event.save(); + + // Send success response + res.status(200).json({ message: 'Event received successfully' }); + } catch (error) { + console.error('Error processing AutoCAD event:', error); + res.status(500).json({ message: 'Error processing event', error: error.message }); + } +}; + +// Get all stored events (for demo/debugging) +exports.getAllEvents = (req, res) => { + res.json(global.autocadEvents); +}; \ No newline at end of file diff --git a/data/events_2025-05-16T12-44-37.278Z.json b/data/events_2025-05-16T12-44-37.278Z.json new file mode 100644 index 0000000..abcdec6 --- /dev/null +++ b/data/events_2025-05-16T12-44-37.278Z.json @@ -0,0 +1,1225 @@ +{ + "all": [ + { + "EventType": "BlockMoved", + "ObjectId": "4492", + "BlockName": "BEACON BASE", + "OldPosition": { + "X": 138207.7475633819, + "Y": 32553.864860076457, + "Z": -12784.449183761575 + }, + "NewPosition": { + "X": 138207.7475633819, + "Y": 38294.674916998774, + "Z": -12784.449183761575 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "CAT": "B-TL70-Q5", + "FAMILY": "BEACON BASE", + "P_TAG1": "UL1_14_BCN1", + "CP": "MCM03" + }, + "Timestamp": "2025-05-16T12:34:32.5872291Z", + "timestamp": "2025-05-16T12:34:32.595Z" + }, + { + "EventType": "BlockMoved", + "ObjectId": "4708", + "BlockName": "*U43", + "OldPosition": { + "X": 169223.2510337755, + "Y": 23023.093734122813, + "Z": -7988.844007427945 + }, + "NewPosition": { + "X": 169223.2510337755, + "Y": 9632.102030208742, + "Z": -7988.844007427945 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "P_TAG1": "UL1_16_ENC1", + "CAT": "RH-P144AJ/8-30 MW1", + "FAMILY": "ENCWH", + "CP": "MCM03", + "INPUT1": "", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:36:56.1493116Z", + "timestamp": "2025-05-16T12:36:56.150Z" + }, + { + "EventType": "BlockAttributesChanged", + "ObjectId": "4708", + "BlockName": "*U43", + "Position": { + "X": 169223.2510337755, + "Y": 9632.102030208742, + "Z": -7988.844007427945 + }, + "AttributeChanges": { + "P_TAG1": { + "OldValue": "UL1_16_ENC1", + "NewValue": "123123" + } + }, + "Attributes": { + "P_TAG1": "123123", + "CAT": "RH-P144AJ/8-30 MW1", + "FAMILY": "ENCWH", + "CP": "MCM03", + "INPUT1": "", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:37:03.1783907Z", + "timestamp": "2025-05-16T12:37:03.180Z" + }, + { + "EventType": "BlockMoved", + "ObjectId": "46E4", + "BlockName": "DPM", + "OldPosition": { + "X": 203382.15662062913, + "Y": 62874.47545441054, + "Z": 74.85662736518134 + }, + "NewPosition": { + "X": 178397.51391081978, + "Y": 62874.47545441054, + "Z": 74.85662736518134 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "TAG": "NCP1_1_DPM1", + "INST": "MCM03" + }, + "Timestamp": "2025-05-16T12:37:13.9908885Z", + "timestamp": "2025-05-16T12:37:13.991Z" + }, + { + "EventType": "BlockMoved", + "ObjectId": "447B", + "BlockName": "*U41", + "OldPosition": { + "X": 151047.6952542998, + "Y": 20052.717928796075, + "Z": 74.85662736518134 + }, + "NewPosition": { + "X": 140027.6758813099, + "Y": 20052.717928796075, + "Z": 74.85662736518134 + }, + "OldRotation": 3.1415926535897927, + "NewRotation": 3.1415926535897927, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "CAT": "440E-LL5SE5", + "FAMILY": "EPC", + "CP": "MCM03", + "INPUT1": "NCP-015-SIO1", + "INPUT2": "NCP-015-SIO1", + "P_TAG1": "UL1_14_EPC2", + "MANUFACTURER": "ALLEN BRADLEY" + }, + "Timestamp": "2025-05-16T12:37:38.5562984Z", + "timestamp": "2025-05-16T12:37:38.555Z" + }, + { + "EventType": "BlockAttributesChanged", + "ObjectId": "447B", + "BlockName": "*U41", + "Position": { + "X": 140027.6758813099, + "Y": 20052.717928796075, + "Z": 74.85662736518134 + }, + "AttributeChanges": { + "CAT": { + "OldValue": "440E-LL5SE5", + "NewValue": "440E-LL5SE5123123" + } + }, + "Attributes": { + "CAT": "440E-LL5SE5123123", + "FAMILY": "EPC", + "CP": "MCM03", + "INPUT1": "NCP-015-SIO1", + "INPUT2": "NCP-015-SIO1", + "P_TAG1": "UL1_14_EPC2", + "MANUFACTURER": "ALLEN BRADLEY" + }, + "Timestamp": "2025-05-16T12:37:44.9017402Z", + "timestamp": "2025-05-16T12:37:44.899Z" + }, + { + "EventType": "BlockMoved", + "ObjectId": "44DC", + "BlockName": "CLX_GS", + "OldPosition": { + "X": 147488.29346190393, + "Y": 12651.701956081204, + "Z": 2886.936781437439 + }, + "NewPosition": { + "X": 147488.29346190393, + "Y": 8371.140596884943, + "Z": 2886.936781437439 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "P_TAG1": "UL1_14_S2", + "INPUT1": "", + "OUTPUT1": "", + "FAMILY": "START PB", + "CP": "MCM03", + "CAT": "S22AMTSGRY3Q", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:38:47.1375789Z", + "timestamp": "2025-05-16T12:38:47.139Z" + }, + { + "EventType": "BlockAttributesChanged", + "ObjectId": "44DC", + "BlockName": "CLX_GS", + "Position": { + "X": 147488.29346190393, + "Y": 8371.140596884943, + "Z": 2886.936781437439 + }, + "AttributeChanges": { + "P_TAG1": { + "OldValue": "UL1_14_S2", + "NewValue": "UL1_14_S2klhgluig" + } + }, + "Attributes": { + "P_TAG1": "UL1_14_S2klhgluig", + "INPUT1": "", + "OUTPUT1": "", + "FAMILY": "START PB", + "CP": "MCM03", + "CAT": "S22AMTSGRY3Q", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:38:54.1477445Z", + "timestamp": "2025-05-16T12:38:54.148Z" + }, + { + "EventType": "BlockMoved", + "ObjectId": "46E8", + "BlockName": "*U43", + "OldPosition": { + "X": 151276.69743875042, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "NewPosition": { + "X": 139493.85559625516, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "P_TAG1": "UL1_14_ENC1", + "CAT": "RH-P144AJ/8-30 MW1", + "FAMILY": "ENCWH", + "CP": "MCM03", + "INPUT1": "", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:39:38.7155427Z", + "timestamp": "2025-05-16T12:39:38.715Z" + }, + { + "EventType": "BlockAttributesChanged", + "ObjectId": "46E8", + "BlockName": "*U43", + "Position": { + "X": 139493.85559625516, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "AttributeChanges": { + "P_TAG1": { + "OldValue": "UL1_14_ENC1", + "NewValue": "UL1_14_ENC1123123" + } + }, + "Attributes": { + "P_TAG1": "UL1_14_ENC1123123", + "CAT": "RH-P144AJ/8-30 MW1", + "FAMILY": "ENCWH", + "CP": "MCM03", + "INPUT1": "", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:39:45.43826Z", + "timestamp": "2025-05-16T12:39:45.436Z" + }, + { + "EventType": "Added", + "ObjectId": "5056", + "ObjectType": "Circle", + "Position": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "Timestamp": "2025-05-16T12:43:03.7829141Z", + "timestamp": "2025-05-16T12:43:03.802Z" + }, + { + "EventType": "Added", + "ObjectId": "5058", + "ObjectType": "Polyline", + "Position": { + "X": -11.745097913741802, + "Y": -2.4042279011050596, + "Z": 0 + }, + "Timestamp": "2025-05-16T12:43:03.8063674Z", + "timestamp": "2025-05-16T12:43:03.808Z" + }, + { + "EventType": "Added", + "ObjectId": "5059", + "ObjectType": "Polyline", + "Position": { + "X": 0.016559234109763565, + "Y": -2.4042279011050596, + "Z": 0 + }, + "Timestamp": "2025-05-16T12:43:03.8080385Z", + "timestamp": "2025-05-16T12:43:03.809Z" + }, + { + "EventType": "BlockModified", + "ObjectId": "46E8", + "BlockName": "*U76", + "OldPosition": { + "X": 139493.85559625516, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "NewPosition": { + "X": 139493.85559625516, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "P_TAG1": "UL1_14_ENC1123123", + "CAT": "RH-P144AJ/8-30 MW1", + "FAMILY": "ENCWH", + "CP": "MCM03", + "INPUT1": "", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:43:03.8319063Z", + "timestamp": "2025-05-16T12:43:03.833Z" + }, + { + "EventType": "BlockModified", + "ObjectId": "46E8", + "BlockName": "*U76", + "OldPosition": { + "X": 139493.85559625516, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "NewPosition": { + "X": 139493.85559625516, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "P_TAG1": "UL1_14_ENC1123123", + "CAT": "RH-P144AJ/8-30 MW1", + "FAMILY": "ENCWH", + "CP": "MCM03", + "INPUT1": "", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:43:03.8373275Z", + "timestamp": "2025-05-16T12:43:03.838Z" + }, + { + "EventType": "BlockAdded", + "ObjectId": "5069", + "BlockName": "conveyor_angled_180", + "Position": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "Scale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "Rotation": 0, + "Attributes": { + "NAME": "" + }, + "Timestamp": "2025-05-16T12:44:31.4134494Z", + "timestamp": "2025-05-16T12:44:31.419Z" + }, + { + "EventType": "BlockModified", + "ObjectId": "5069", + "BlockName": "conveyor_angled_180", + "OldPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "NewPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "NewScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "AttributeChanges": null, + "Attributes": { + "NAME": "" + }, + "Timestamp": "2025-05-16T12:44:31.4196895Z", + "timestamp": "2025-05-16T12:44:31.421Z" + }, + { + "EventType": "BlockModified", + "ObjectId": "5069", + "BlockName": "conveyor_angled_180", + "OldPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "NewPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "NewScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "AttributeChanges": null, + "Attributes": { + "NAME": "" + }, + "Timestamp": "2025-05-16T12:44:31.4216077Z", + "timestamp": "2025-05-16T12:44:31.424Z" + }, + { + "EventType": "BlockModified", + "ObjectId": "5069", + "BlockName": "conveyor_angled_180", + "OldPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "NewPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "NewScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "AttributeChanges": null, + "Attributes": { + "NAME": "" + }, + "Timestamp": "2025-05-16T12:44:31.5920388Z", + "timestamp": "2025-05-16T12:44:31.594Z" + }, + { + "EventType": "BlockAttributesChanged", + "ObjectId": "5069", + "BlockName": "conveyor_angled_180", + "OldPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "NewPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "NewScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "AttributeChanges": { + "NAME": { + "OldValue": "", + "NewValue": "asdasd" + } + }, + "Attributes": { + "NAME": "asdasd" + }, + "Timestamp": "2025-05-16T12:44:37.2727004Z", + "timestamp": "2025-05-16T12:44:37.274Z" + } + ], + "blockEvents": { + "added": [ + { + "EventType": "BlockAdded", + "ObjectId": "5069", + "BlockName": "conveyor_angled_180", + "Position": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "Scale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "Rotation": 0, + "Attributes": { + "NAME": "" + }, + "Timestamp": "2025-05-16T12:44:31.4134494Z", + "timestamp": "2025-05-16T12:44:31.419Z" + } + ], + "erased": [], + "stretched": [], + "rotated": [], + "scaled": [], + "moved": [ + { + "EventType": "BlockMoved", + "ObjectId": "4492", + "BlockName": "BEACON BASE", + "OldPosition": { + "X": 138207.7475633819, + "Y": 32553.864860076457, + "Z": -12784.449183761575 + }, + "NewPosition": { + "X": 138207.7475633819, + "Y": 38294.674916998774, + "Z": -12784.449183761575 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "CAT": "B-TL70-Q5", + "FAMILY": "BEACON BASE", + "P_TAG1": "UL1_14_BCN1", + "CP": "MCM03" + }, + "Timestamp": "2025-05-16T12:34:32.5872291Z", + "timestamp": "2025-05-16T12:34:32.595Z" + }, + { + "EventType": "BlockMoved", + "ObjectId": "4708", + "BlockName": "*U43", + "OldPosition": { + "X": 169223.2510337755, + "Y": 23023.093734122813, + "Z": -7988.844007427945 + }, + "NewPosition": { + "X": 169223.2510337755, + "Y": 9632.102030208742, + "Z": -7988.844007427945 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "P_TAG1": "UL1_16_ENC1", + "CAT": "RH-P144AJ/8-30 MW1", + "FAMILY": "ENCWH", + "CP": "MCM03", + "INPUT1": "", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:36:56.1493116Z", + "timestamp": "2025-05-16T12:36:56.150Z" + }, + { + "EventType": "BlockMoved", + "ObjectId": "46E4", + "BlockName": "DPM", + "OldPosition": { + "X": 203382.15662062913, + "Y": 62874.47545441054, + "Z": 74.85662736518134 + }, + "NewPosition": { + "X": 178397.51391081978, + "Y": 62874.47545441054, + "Z": 74.85662736518134 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "TAG": "NCP1_1_DPM1", + "INST": "MCM03" + }, + "Timestamp": "2025-05-16T12:37:13.9908885Z", + "timestamp": "2025-05-16T12:37:13.991Z" + }, + { + "EventType": "BlockMoved", + "ObjectId": "447B", + "BlockName": "*U41", + "OldPosition": { + "X": 151047.6952542998, + "Y": 20052.717928796075, + "Z": 74.85662736518134 + }, + "NewPosition": { + "X": 140027.6758813099, + "Y": 20052.717928796075, + "Z": 74.85662736518134 + }, + "OldRotation": 3.1415926535897927, + "NewRotation": 3.1415926535897927, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "CAT": "440E-LL5SE5", + "FAMILY": "EPC", + "CP": "MCM03", + "INPUT1": "NCP-015-SIO1", + "INPUT2": "NCP-015-SIO1", + "P_TAG1": "UL1_14_EPC2", + "MANUFACTURER": "ALLEN BRADLEY" + }, + "Timestamp": "2025-05-16T12:37:38.5562984Z", + "timestamp": "2025-05-16T12:37:38.555Z" + }, + { + "EventType": "BlockMoved", + "ObjectId": "44DC", + "BlockName": "CLX_GS", + "OldPosition": { + "X": 147488.29346190393, + "Y": 12651.701956081204, + "Z": 2886.936781437439 + }, + "NewPosition": { + "X": 147488.29346190393, + "Y": 8371.140596884943, + "Z": 2886.936781437439 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "P_TAG1": "UL1_14_S2", + "INPUT1": "", + "OUTPUT1": "", + "FAMILY": "START PB", + "CP": "MCM03", + "CAT": "S22AMTSGRY3Q", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:38:47.1375789Z", + "timestamp": "2025-05-16T12:38:47.139Z" + }, + { + "EventType": "BlockMoved", + "ObjectId": "46E8", + "BlockName": "*U43", + "OldPosition": { + "X": 151276.69743875042, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "NewPosition": { + "X": 139493.85559625516, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "P_TAG1": "UL1_14_ENC1", + "CAT": "RH-P144AJ/8-30 MW1", + "FAMILY": "ENCWH", + "CP": "MCM03", + "INPUT1": "", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:39:38.7155427Z", + "timestamp": "2025-05-16T12:39:38.715Z" + } + ], + "attributesChanged": [ + { + "EventType": "BlockAttributesChanged", + "ObjectId": "4708", + "BlockName": "*U43", + "Position": { + "X": 169223.2510337755, + "Y": 9632.102030208742, + "Z": -7988.844007427945 + }, + "AttributeChanges": { + "P_TAG1": { + "OldValue": "UL1_16_ENC1", + "NewValue": "123123" + } + }, + "Attributes": { + "P_TAG1": "123123", + "CAT": "RH-P144AJ/8-30 MW1", + "FAMILY": "ENCWH", + "CP": "MCM03", + "INPUT1": "", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:37:03.1783907Z", + "timestamp": "2025-05-16T12:37:03.180Z" + }, + { + "EventType": "BlockAttributesChanged", + "ObjectId": "447B", + "BlockName": "*U41", + "Position": { + "X": 140027.6758813099, + "Y": 20052.717928796075, + "Z": 74.85662736518134 + }, + "AttributeChanges": { + "CAT": { + "OldValue": "440E-LL5SE5", + "NewValue": "440E-LL5SE5123123" + } + }, + "Attributes": { + "CAT": "440E-LL5SE5123123", + "FAMILY": "EPC", + "CP": "MCM03", + "INPUT1": "NCP-015-SIO1", + "INPUT2": "NCP-015-SIO1", + "P_TAG1": "UL1_14_EPC2", + "MANUFACTURER": "ALLEN BRADLEY" + }, + "Timestamp": "2025-05-16T12:37:44.9017402Z", + "timestamp": "2025-05-16T12:37:44.899Z" + }, + { + "EventType": "BlockAttributesChanged", + "ObjectId": "44DC", + "BlockName": "CLX_GS", + "Position": { + "X": 147488.29346190393, + "Y": 8371.140596884943, + "Z": 2886.936781437439 + }, + "AttributeChanges": { + "P_TAG1": { + "OldValue": "UL1_14_S2", + "NewValue": "UL1_14_S2klhgluig" + } + }, + "Attributes": { + "P_TAG1": "UL1_14_S2klhgluig", + "INPUT1": "", + "OUTPUT1": "", + "FAMILY": "START PB", + "CP": "MCM03", + "CAT": "S22AMTSGRY3Q", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:38:54.1477445Z", + "timestamp": "2025-05-16T12:38:54.148Z" + }, + { + "EventType": "BlockAttributesChanged", + "ObjectId": "46E8", + "BlockName": "*U43", + "Position": { + "X": 139493.85559625516, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "AttributeChanges": { + "P_TAG1": { + "OldValue": "UL1_14_ENC1", + "NewValue": "UL1_14_ENC1123123" + } + }, + "Attributes": { + "P_TAG1": "UL1_14_ENC1123123", + "CAT": "RH-P144AJ/8-30 MW1", + "FAMILY": "ENCWH", + "CP": "MCM03", + "INPUT1": "", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:39:45.43826Z", + "timestamp": "2025-05-16T12:39:45.436Z" + }, + { + "EventType": "BlockAttributesChanged", + "ObjectId": "5069", + "BlockName": "conveyor_angled_180", + "OldPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "NewPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "NewScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "AttributeChanges": { + "NAME": { + "OldValue": "", + "NewValue": "asdasd" + } + }, + "Attributes": { + "NAME": "asdasd" + }, + "Timestamp": "2025-05-16T12:44:37.2727004Z", + "timestamp": "2025-05-16T12:44:37.274Z" + } + ], + "modified": [ + { + "EventType": "BlockModified", + "ObjectId": "46E8", + "BlockName": "*U76", + "OldPosition": { + "X": 139493.85559625516, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "NewPosition": { + "X": 139493.85559625516, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "P_TAG1": "UL1_14_ENC1123123", + "CAT": "RH-P144AJ/8-30 MW1", + "FAMILY": "ENCWH", + "CP": "MCM03", + "INPUT1": "", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:43:03.8319063Z", + "timestamp": "2025-05-16T12:43:03.833Z" + }, + { + "EventType": "BlockModified", + "ObjectId": "46E8", + "BlockName": "*U76", + "OldPosition": { + "X": 139493.85559625516, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "NewPosition": { + "X": 139493.85559625516, + "Y": 23003.520257038996, + "Z": -7988.844007427945 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "NewScale": { + "X": 107.72193349634998, + "Y": 107.72193349634998, + "Z": 107.72193349634998 + }, + "AttributeChanges": null, + "Attributes": { + "P_TAG1": "UL1_14_ENC1123123", + "CAT": "RH-P144AJ/8-30 MW1", + "FAMILY": "ENCWH", + "CP": "MCM03", + "INPUT1": "", + "LOC": "" + }, + "Timestamp": "2025-05-16T12:43:03.8373275Z", + "timestamp": "2025-05-16T12:43:03.838Z" + }, + { + "EventType": "BlockModified", + "ObjectId": "5069", + "BlockName": "conveyor_angled_180", + "OldPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "NewPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "NewScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "AttributeChanges": null, + "Attributes": { + "NAME": "" + }, + "Timestamp": "2025-05-16T12:44:31.4196895Z", + "timestamp": "2025-05-16T12:44:31.421Z" + }, + { + "EventType": "BlockModified", + "ObjectId": "5069", + "BlockName": "conveyor_angled_180", + "OldPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "NewPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "NewScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "AttributeChanges": null, + "Attributes": { + "NAME": "" + }, + "Timestamp": "2025-05-16T12:44:31.4216077Z", + "timestamp": "2025-05-16T12:44:31.424Z" + }, + { + "EventType": "BlockModified", + "ObjectId": "5069", + "BlockName": "conveyor_angled_180", + "OldPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "NewPosition": { + "X": 243133.8885279341, + "Y": 111491.60655969387, + "Z": 0 + }, + "OldRotation": 0, + "NewRotation": 0, + "OldScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "NewScale": { + "X": 1, + "Y": 1, + "Z": 1 + }, + "AttributeChanges": null, + "Attributes": { + "NAME": "" + }, + "Timestamp": "2025-05-16T12:44:31.5920388Z", + "timestamp": "2025-05-16T12:44:31.594Z" + } + ] + }, + "entityEvents": { + "added": [ + { + "EventType": "Added", + "ObjectId": "5056", + "ObjectType": "Circle", + "Position": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "Timestamp": "2025-05-16T12:43:03.7829141Z", + "timestamp": "2025-05-16T12:43:03.802Z" + }, + { + "EventType": "Added", + "ObjectId": "5058", + "ObjectType": "Polyline", + "Position": { + "X": -11.745097913741802, + "Y": -2.4042279011050596, + "Z": 0 + }, + "Timestamp": "2025-05-16T12:43:03.8063674Z", + "timestamp": "2025-05-16T12:43:03.808Z" + }, + { + "EventType": "Added", + "ObjectId": "5059", + "ObjectType": "Polyline", + "Position": { + "X": 0.016559234109763565, + "Y": -2.4042279011050596, + "Z": 0 + }, + "Timestamp": "2025-05-16T12:43:03.8080385Z", + "timestamp": "2025-05-16T12:43:03.809Z" + } + ], + "erased": [], + "stretched": [], + "modified": [] + } +} \ No newline at end of file diff --git a/models/AutocadEvent.js b/models/AutocadEvent.js new file mode 100644 index 0000000..13ff53d --- /dev/null +++ b/models/AutocadEvent.js @@ -0,0 +1,39 @@ +const mongoose = require('mongoose'); + +// Define schema for position data +const positionSchema = new mongoose.Schema({ + X: Number, + Y: Number, + Z: Number +}, { _id: false }); + +// Define schema for AutoCAD events +const autocadEventSchema = new mongoose.Schema({ + EventType: { + type: String, + required: true, + enum: ['Added', 'Modified', 'Deleted', 'Stretched'] + }, + ObjectId: { + type: String, + required: true + }, + ObjectType: { + type: String, + required: true + }, + Position: positionSchema, + OldPosition: positionSchema, + NewPosition: positionSchema, + Command: String, // Store command information + Timestamp: { + type: Date, + required: true + }, + receivedAt: { + type: Date, + default: Date.now + } +}); + +module.exports = mongoose.model('AutocadEvent', autocadEventSchema); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..18bde8b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1594 @@ +{ + "name": "autocadserver", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "autocadserver", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "mongoose": "^7.4.3", + "node-fetch": "^2.7.0" + }, + "devDependencies": { + "nodemon": "^3.1.10" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", + "integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==", + "license": "MIT", + "optional": true, + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/node": { + "version": "22.15.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", + "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", + "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/kareem": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", + "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT", + "optional": true + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mongodb": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", + "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", + "license": "Apache-2.0", + "dependencies": { + "bson": "^5.5.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=14.20.1" + }, + "optionalDependencies": { + "@mongodb-js/saslprep": "^1.1.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.0.0", + "kerberos": "^1.0.0 || ^2.0.0", + "mongodb-client-encryption": ">=2.3.0 <3", + "snappy": "^7.2.2" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongoose": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.8.7.tgz", + "integrity": "sha512-5Bo4CrUxrPITrhMKsqUTOkXXo2CoRC5tXxVQhnddCzqDMwRXfyStrxj1oY865g8gaekSBhxAeNkYyUSJvGm9Hw==", + "license": "MIT", + "dependencies": { + "bson": "^5.5.0", + "kareem": "2.5.1", + "mongodb": "5.9.2", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "16.0.1" + }, + "engines": { + "node": ">=14.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", + "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==", + "license": "MIT" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3b4ed1f --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "autocadserver", + "version": "1.0.0", + "description": "Server for receiving AutoCAD change events", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "autocad", + "server", + "event-tracking" + ], + "author": "", + "license": "ISC", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "mongoose": "^7.4.3", + "node-fetch": "^2.7.0" + }, + "devDependencies": { + "nodemon": "^3.1.10" + } +} diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..00dcc9b --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,225 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 20px; + background: #f9f9f9; +} + +h1 { + color: #333; +} + +.container { + display: flex; + flex-direction: row; +} + +.block-list { + width: 250px; + margin-right: 20px; +} + +.block-item { + padding: 10px; + margin: 5px 0; + background: #f5f5f5; + border-radius: 4px; + cursor: pointer; + border-left: 4px solid #2196F3; + transition: all 0.3s ease; +} + +.block-item:hover { + background: #e0e0e0; +} + +.block-item.active { + background: #e3f2fd; +} + +.block-item.recently-updated { + animation: pulse 1.5s; + border-left-color: #e91e63; +} + +@keyframes pulse { + 0% { background-color: #f5f5f5; } + 50% { background-color: #ffcdd2; } + 100% { background-color: #f5f5f5; } +} + +.timeline-view { + flex: 1; +} + +.timeline-item { + border-left: 2px solid #2196F3; + padding: 10px; + margin: 10px 0 10px 20px; + position: relative; +} + +.timeline-item:before { + content: ''; + position: absolute; + width: 12px; + height: 12px; + background: #2196F3; + border-radius: 50%; + left: -7px; + top: 10px; +} + +.timeline-item.attribute-edit { + border-left-color: #e91e63; +} + +.timeline-item.attribute-edit:before { + background: #e91e63; +} + +.timeline-item.moved { + border-left-color: #4caf50; +} + +.timeline-item.moved:before { + background: #4caf50; +} + +.timeline-item.scaled { + border-left-color: #ff9800; +} + +.timeline-item.scaled:before { + background: #ff9800; +} + +.timeline-item.stretched { + border-left-color: #9c27b0; +} + +.timeline-item.stretched:before { + background: #9c27b0; +} + +.timeline-item.rotated { + border-left-color: #795548; +} + +.timeline-item.rotated:before { + background: #795548; +} + +.timeline-time { + color: #666; + font-size: 0.8em; +} + +.action-type { + font-weight: bold; + margin-right: 10px; +} + +.block-action-details { + margin-top: 5px; + padding: 8px; + background: #f5f5f5; + border-radius: 4px; +} + +.timeline-container { + max-height: 600px; + overflow-y: auto; + padding-right: 10px; +} + +.block-info { + margin-bottom: 15px; + padding: 10px; + background: #f5f5f5; + border-radius: 4px; +} + +.changes { + color: #e91e63; + font-weight: bold; +} + +.current { + color: #4caf50; + font-weight: bold; +} + +.initial { + color: #2196F3; + font-weight: bold; +} + +.attribute { + color: #333; +} + +.attribute-section { + border-top: 1px solid #ddd; + margin-top: 8px; + padding-top: 8px; +} + +.attribute-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px; +} + +@media (min-width: 768px) { + .attribute-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +.collapsible { + cursor: pointer; + padding: 5px 0; + width: 100%; + border: none; + text-align: left; + outline: none; + background-color: transparent; + font-weight: bold; + position: relative; +} + +.collapsible:after { + content: '+'; + font-weight: bold; + float: right; + margin-left: 5px; +} + +.active-collapse:after { + content: "-"; +} + +.collapse-content { + max-height: 0; + overflow: hidden; + transition: max-height 0.2s ease-out; +} + +.notification { + position: fixed; + bottom: 20px; + right: 20px; + padding: 10px 20px; + background-color: #4caf50; + color: white; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + opacity: 0; + transition: opacity 0.3s; + z-index: 1000; +} + +.notification.show { + opacity: 1; +} \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..c2d55f3 --- /dev/null +++ b/public/index.html @@ -0,0 +1,103 @@ + + + + + + AutoCAD Block Timeline + + + + +
+
Not connected
+ +
+ +

AutoCAD Block Timeline

+ +
+
+

Blocks

+
+
+ +
+
+
+
+
+ +
+ +
+ + + + \ No newline at end of file diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..9bc7648 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,532 @@ +// Store the current blocks for comparison +window.currentBlocks = {}; +window.selectedBlockId = null; +window.eventSource = null; +window.maxReconnectAttempts = 5; +window.reconnectAttempts = 0; +window.reconnectTimeout = null; +window.serverConnected = false; + +// Fetch blocks on page load +window.onload = function() { + fetchBlocks(); + setupSSEConnection(); + + // Add manual reconnect button listeners + const reconnectButton = document.getElementById('reconnect-button'); + if (reconnectButton) { + reconnectButton.addEventListener('click', function() { + window.reconnectAttempts = 0; + setupSSEConnection(); + }); + } else { + console.error('Reconnect button not found in DOM'); + } +}; + +// Set up SSE connection for real-time updates +function setupSSEConnection() { + // Clear any existing SSE connection + if (window.eventSource) { + window.eventSource.close(); + window.eventSource = null; + } + + // Clear any pending reconnect timeout + if (window.reconnectTimeout) { + clearTimeout(window.reconnectTimeout); + window.reconnectTimeout = null; + } + + // Update connection status UI + updateConnectionStatus('connecting'); + + // First check if server is reachable before attempting SSE connection + fetch('/api/server-status') + .then(response => { + if (!response.ok) { + throw new Error('Server returned status: ' + response.status); + } + return response.json(); + }) + .then(data => { + console.log('Server is online:', data); + // If server check succeeds, now establish SSE connection + connectEventSource(); + }) + .catch(error => { + console.error('Server status check failed:', error); + updateConnectionStatus('failed'); + + // Only attempt to reconnect if we haven't exceeded max attempts + if (window.reconnectAttempts < window.maxReconnectAttempts) { + window.reconnectAttempts++; + + // Increase delay with each attempt (exponential backoff) + const delay = Math.min(30000, 1000 * Math.pow(2, window.reconnectAttempts)); + + console.log(`Reconnect attempt ${window.reconnectAttempts}/${window.maxReconnectAttempts} in ${delay/1000} seconds`); + updateConnectionStatus('waiting', delay/1000); + + // Try to reconnect after a delay + window.reconnectTimeout = setTimeout(() => { + setupSSEConnection(); + }, delay); + } else { + console.log('Max reconnect attempts reached. Please reconnect manually.'); + updateConnectionStatus('failed'); + } + }); +} + +// Function to establish the actual EventSource connection +function connectEventSource() { + try { + // Set up new SSE connection + window.eventSource = new EventSource('/api/sse'); + + // Handle connection event + window.eventSource.addEventListener('connected', function(e) { + console.log('SSE Connection established'); + window.reconnectAttempts = 0; + window.serverConnected = true; + updateConnectionStatus('connected'); + }); + + // Handle real-time events + window.eventSource.addEventListener('new-event', function(e) { + try { + const newEvent = JSON.parse(e.data); + + // Check if this is a block event + if (newEvent.EventType && newEvent.EventType.startsWith('Block')) { + console.log('Received real-time update:', newEvent); + + // Always refresh the blocks list to catch new/updated blocks + fetchBlocks(); + + // Show notification + showNotification(`${newEvent.BlockName}: ${newEvent.EventType}`); + + // If this event is for the currently selected block, update its timeline + if (window.selectedBlockId && newEvent.ObjectId === window.selectedBlockId) { + fetchBlockEvents(window.selectedBlockId); + } + } + } catch (error) { + console.error('Error processing real-time event:', error); + } + }); + + // Handle connection errors + window.eventSource.addEventListener('error', function(e) { + console.error('SSE connection error:', e); + window.serverConnected = false; + + // Close the connection + if (window.eventSource) { + window.eventSource.close(); + window.eventSource = null; + } + + // Update connection status + updateConnectionStatus('disconnected'); + + // Only attempt to reconnect if we haven't exceeded max attempts + if (window.reconnectAttempts < window.maxReconnectAttempts) { + window.reconnectAttempts++; + + // Increase delay with each attempt (exponential backoff) + const delay = Math.min(30000, 1000 * Math.pow(2, window.reconnectAttempts)); + + console.log(`Reconnect attempt ${window.reconnectAttempts}/${window.maxReconnectAttempts} in ${delay/1000} seconds`); + updateConnectionStatus('waiting', delay/1000); + + // Try to reconnect after a delay + window.reconnectTimeout = setTimeout(() => { + setupSSEConnection(); + }, delay); + } else { + console.log('Max reconnect attempts reached. Please reconnect manually.'); + updateConnectionStatus('failed'); + } + }); + } catch (error) { + console.error('Error setting up SSE connection:', error); + updateConnectionStatus('failed'); + } + + // When leaving the page, close the connection + window.addEventListener('beforeunload', function() { + if (window.eventSource) { + window.eventSource.close(); + window.eventSource = null; + } + }); +} + +// Function to update connection status UI +function updateConnectionStatus(status, seconds) { + const statusElement = document.getElementById('connection-status'); + const reconnectButton = document.getElementById('reconnect-button'); + + if (!statusElement) return; + + statusElement.className = 'status-indicator ' + status; + + switch(status) { + case 'connected': + statusElement.textContent = 'Connected'; + reconnectButton.style.display = 'none'; + break; + case 'connecting': + statusElement.textContent = 'Connecting...'; + reconnectButton.style.display = 'none'; + break; + case 'disconnected': + statusElement.textContent = 'Disconnected'; + reconnectButton.style.display = 'inline-block'; + break; + case 'waiting': + statusElement.textContent = `Reconnecting in ${seconds}s`; + reconnectButton.style.display = 'inline-block'; + break; + case 'failed': + statusElement.textContent = 'Connection failed'; + reconnectButton.style.display = 'inline-block'; + break; + default: + statusElement.textContent = 'Unknown status'; + reconnectButton.style.display = 'inline-block'; + } +} + +// Fetch all blocks +function fetchBlocks() { + fetch('/api/blocks') + .then(response => response.json()) + .then(blocks => { + const blocksDiv = document.getElementById('blocks'); + + // Save currently selected block ID + const currentlySelectedId = window.selectedBlockId; + + // Store new blocks list but keep track of which ones are new/updated + const updatedBlocks = {}; + + // Prepare to rebuild the blocks list + blocksDiv.innerHTML = ''; + + if (blocks.length === 0) { + blocksDiv.innerHTML = '

No blocks found

'; + return; + } + + blocks.forEach(block => { + const blockDiv = document.createElement('div'); + blockDiv.className = 'block-item'; + + // Check if this block was recently updated + const existingBlock = window.currentBlocks[block.id]; + const isUpdated = existingBlock && + (existingBlock.lastEventType !== block.lastEventType || + existingBlock.timestamp !== block.timestamp); + + // Check if this is a new block (not in currentBlocks) + const isNew = !window.currentBlocks[block.id]; + + if (isUpdated) { + blockDiv.classList.add('recently-updated'); + } + + if (isNew) { + blockDiv.classList.add('recently-updated'); + showNotification(`New block detected: ${block.name}`); + } + + // Mark as active if this was the selected block + if (currentlySelectedId && currentlySelectedId === block.id) { + blockDiv.classList.add('active'); + } + + blockDiv.dataset.id = block.id; + blockDiv.onclick = () => { + // Set active class + document.querySelectorAll('.block-item').forEach(item => { + item.classList.remove('active'); + }); + blockDiv.classList.add('active'); + + // Store selected block ID + window.selectedBlockId = block.id; + + // Load timeline + loadBlockTimeline(block.id, block.name); + }; + + blockDiv.innerHTML = ` + ${block.name} (ID: ${block.id}) + Last update: ${timeAgo(new Date(block.timestamp))} + `; + + blocksDiv.appendChild(blockDiv); + + // Store this block for future comparison + updatedBlocks[block.id] = block; + }); + + // Update the stored blocks list + window.currentBlocks = updatedBlocks; + }) + .catch(error => console.error('Error fetching blocks:', error)); +} + +// Helper function to format time ago +function timeAgo(date) { + const seconds = Math.floor((new Date() - date) / 1000); + + let interval = Math.floor(seconds / 31536000); + if (interval > 1) return interval + ' years ago'; + if (interval === 1) return 'a year ago'; + + interval = Math.floor(seconds / 2592000); + if (interval > 1) return interval + ' months ago'; + if (interval === 1) return 'a month ago'; + + interval = Math.floor(seconds / 86400); + if (interval > 1) return interval + ' days ago'; + if (interval === 1) return 'a day ago'; + + interval = Math.floor(seconds / 3600); + if (interval > 1) return interval + ' hours ago'; + if (interval === 1) return 'an hour ago'; + + interval = Math.floor(seconds / 60); + if (interval > 1) return interval + ' minutes ago'; + if (interval === 1) return 'a minute ago'; + + if (seconds < 10) return 'just now'; + + return Math.floor(seconds) + ' seconds ago'; +} + +// Load block timeline +function loadBlockTimeline(blockId, blockName) { + // Display block info + const blockInfo = document.getElementById('blockInfo'); + blockInfo.innerHTML = ` +

${blockName}

+

Block ID: ${blockId}

+

Real-time updates enabled

+ `; + + // Load block events + fetchBlockEvents(blockId); +} + +// Fetch block events +function fetchBlockEvents(blockId) { + // Track current event count + const currentEvents = document.querySelectorAll('.timeline-item').length; + + // Fetch block events + fetch('/api/events') + .then(response => response.json()) + .then(events => { + // Filter events for this block + const blockEvents = events.filter(event => + event.ObjectId === blockId && event.EventType && event.EventType.startsWith('Block') + ).sort((a, b) => new Date(a.Timestamp) - new Date(b.Timestamp)); + + // Check if we have new events + if (blockEvents.length > currentEvents && currentEvents > 0) { + showNotification(`${blockEvents.length - currentEvents} new event(s) received`); + } + + const timelineDiv = document.getElementById('blockTimeline'); + + // Store the scroll position + const scrollPosition = timelineDiv.scrollTop; + + // Clear current timeline + timelineDiv.innerHTML = ''; + + if (blockEvents.length === 0) { + timelineDiv.innerHTML = '

No events recorded for this block

'; + return; + } + + // Process events + blockEvents.forEach(event => { + const date = new Date(event.Timestamp); + + // Create timeline item + const timelineItem = document.createElement('div'); + + // Set appropriate class based on event type + let eventClass = ''; + let actionLabel = ''; + + switch(event.EventType) { + case 'BlockAttributesChanged': + eventClass = 'attribute-edit'; + actionLabel = 'Attribute Edit'; + break; + case 'BlockMoved': + eventClass = 'moved'; + actionLabel = 'Moved'; + break; + case 'BlockRotated': + eventClass = 'rotated'; + actionLabel = 'Rotated'; + break; + case 'BlockScaled': + eventClass = 'scaled'; + actionLabel = 'Scaled'; + break; + case 'BlockStretched': + eventClass = 'stretched'; + actionLabel = 'Stretched'; + break; + default: + eventClass = ''; + actionLabel = event.EventType.replace('Block', ''); + } + + timelineItem.className = `timeline-item ${eventClass}`; + + // Create content for timeline item + let timelineContent = ` +
${date.toLocaleString()}
+ ${actionLabel} + `; + + // Add position information - always show current position + let position = event.NewPosition || event.Position; + if (position) { + timelineContent += ` +
+ Position: (${position.X.toFixed(2)}, ${position.Y.toFixed(2)}, ${position.Z.toFixed(2)}) + `; + + // Add old position if this was a move event + if (event.EventType === 'BlockMoved' && event.OldPosition) { + timelineContent += ` +
Old Position: (${event.OldPosition.X.toFixed(2)}, ${event.OldPosition.Y.toFixed(2)}, ${event.OldPosition.Z.toFixed(2)}) + `; + } + + timelineContent += '
'; + } + + // Add attributes - always show all attributes + if (event.Attributes && Object.keys(event.Attributes).length > 0) { + timelineContent += '
'; + timelineContent += ''; + timelineContent += '
'; + timelineContent += '
'; + + // Sort the attributes by key for consistent display + const sortedAttributes = Object.keys(event.Attributes).sort().reduce( + (obj, key) => { + obj[key] = event.Attributes[key]; + return obj; + }, + {} + ); + + for (const [key, value] of Object.entries(sortedAttributes)) { + // Check if this attribute was changed in this event + let attributeClass = 'attribute'; + if (event.AttributeChanges && event.AttributeChanges[key]) { + attributeClass = 'changes'; + } + + timelineContent += ` +
+ ${key}: ${value} +
+ `; + } + + timelineContent += '
'; // Close attribute-grid + timelineContent += '
'; // Close collapse-content + + // If this was an attribute change, show what changed + if (event.EventType === 'BlockAttributesChanged' && event.AttributeChanges) { + timelineContent += '
Changed Values:'; + + for (const [key, change] of Object.entries(event.AttributeChanges)) { + timelineContent += ` +
+ ${key}: + ${change.OldValue} → + ${change.NewValue} +
+ `; + } + + timelineContent += '
'; // Close attribute-section + } + + timelineContent += '
'; // Close block-action-details + } + + // Add other transformation details if available + if (event.EventType === 'BlockRotated' && event.OldRotation !== undefined && event.NewRotation !== undefined) { + timelineContent += ` +
+ Rotation: ${(event.NewRotation * 180 / Math.PI).toFixed(2)}° +
Old Rotation: ${(event.OldRotation * 180 / Math.PI).toFixed(2)}° +
+ `; + } else if (event.EventType === 'BlockScaled' && event.OldScale && event.NewScale) { + timelineContent += ` +
+ Scale: X=${event.NewScale.X.toFixed(3)}, Y=${event.NewScale.Y.toFixed(3)}, Z=${event.NewScale.Z.toFixed(3)} +
Old Scale: X=${event.OldScale.X.toFixed(3)}, Y=${event.OldScale.Y.toFixed(3)}, Z=${event.OldScale.Z.toFixed(3)} +
+ `; + } + + timelineItem.innerHTML = timelineContent; + timelineDiv.appendChild(timelineItem); + }); + + // Restore scroll position + timelineDiv.scrollTop = scrollPosition; + + // Initialize collapsible sections + activateCollapsibles(); + }) + .catch(error => { + document.getElementById('blockTimeline').innerHTML = `

Error: ${error.message}

`; + }); +} + +// Helper function to activate collapsible sections after refresh +function activateCollapsibles() { + const collapsibles = document.querySelectorAll('.collapsible'); + collapsibles.forEach(collapsible => { + collapsible.addEventListener('click', function() { + this.classList.toggle('active-collapse'); + const content = this.nextElementSibling; + if (content.style.maxHeight) { + content.style.maxHeight = null; + } else { + content.style.maxHeight = content.scrollHeight + "px"; + } + }); + }); +} + +// Function to show notification +function showNotification(message) { + const notification = document.getElementById('notification-message'); + notification.textContent = message; + const notificationDiv = document.getElementById('notification'); + notificationDiv.className = 'notification show'; + setTimeout(() => { + notificationDiv.className = 'notification'; + }, 3000); +} \ No newline at end of file diff --git a/routes/autocadRoutes.js b/routes/autocadRoutes.js new file mode 100644 index 0000000..6ddc8ff --- /dev/null +++ b/routes/autocadRoutes.js @@ -0,0 +1,11 @@ +const express = require('express'); +const router = express.Router(); +const autocadController = require('../controllers/autocadController'); + +// Route to receive AutoCAD events +router.post('/autocad-events', autocadController.receiveEvent); + +// Route to get all stored events (for demo/debugging) +router.get('/autocad-events', autocadController.getAllEvents); + +module.exports = router; \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..1c7358f --- /dev/null +++ b/server.js @@ -0,0 +1,712 @@ +const express = require('express'); +const mongoose = require('mongoose'); +const cors = require('cors'); +const dotenv = require('dotenv'); +const autocadRoutes = require('./routes/autocadRoutes'); +const fs = require('fs'); +const path = require('path'); + +// Load environment variables +dotenv.config(); + +// Initialize express app +const app = express(); + +// Middleware +app.use(cors()); +app.use(express.json({ limit: '10mb' })); // Increase payload limit + +// Set timeout for requests +app.use((req, res, next) => { + res.setTimeout(30000, () => { + console.error('Request timeout'); + res.status(408).send('Request Timeout'); + }); + next(); +}); + +// Error handling middleware +app.use((err, req, res, next) => { + console.error('Server error:', err); + res.status(500).send('Internal Server Error'); +}); + +// Serve static files from the public directory +app.use(express.static('public')); + +// Create storage directory if it doesn't exist +const dataDir = path.join(__dirname, 'data'); +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir); +} + +// Initialize event storage by categories +global.autocadEvents = { + all: [], + blockEvents: { + added: [], + erased: [], + stretched: [], + rotated: [], + scaled: [], + moved: [], + attributesChanged: [], + modified: [] + }, + entityEvents: { + added: [], + erased: [], + stretched: [], + modified: [] + } +}; + +// Add a store of SSE clients for real-time updates +const sseClients = []; + +// Performance monitoring +let eventCount = 0; +let lastSaveTime = Date.now(); +const MAX_EVENTS_IN_MEMORY = 10000; // Limit total events in memory + +// Middleware to handle SSE connections +app.get('/api/sse', (req, res) => { + // Set headers for SSE + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + }); + + // Send initial connection established message + res.write('event: connected\n'); + res.write('data: Connection established\n\n'); + + // Add this client to the list + const clientId = Date.now(); + const newClient = { + id: clientId, + res + }; + sseClients.push(newClient); + + // Handle client disconnect + req.on('close', () => { + console.log(`SSE Client ${clientId} disconnected`); + const index = sseClients.findIndex(client => client.id === clientId); + if (index !== -1) { + sseClients.splice(index, 1); + } + }); +}); + +// Function to notify all SSE clients of a new event +function notifyClientsOfNewEvent(event) { + const failedClients = []; + + sseClients.forEach((client, index) => { + try { + client.res.write(`event: new-event\n`); + client.res.write(`data: ${JSON.stringify(event)}\n\n`); + } catch (error) { + console.error(`Error sending to client ${client.id}:`, error); + failedClients.push(index); + } + }); + + // Remove failed clients (in reverse order to avoid index issues) + failedClients.reverse().forEach(index => { + console.log(`Removing failed SSE client ${sseClients[index].id}`); + sseClients.splice(index, 1); + }); +} + +// Routes +app.use('/api', autocadRoutes); + +// Add connection test endpoint +app.post('/api/connection-test', (req, res) => { + console.log('Connection test received'); + res.status(200).json({ success: true, message: 'Connection successful', serverTime: new Date().toISOString() }); +}); + +// Add custom event handling middleware +app.post('/api/events', (req, res) => { + try { + const eventData = req.body; + + // Handle connection test events specially + if (eventData.EventType === 'ConnectionTest') { + console.log('AutoCAD connection test received'); + return res.status(200).json({ + success: true, + message: 'Connection test successful', + serverTime: new Date().toISOString(), + eventsCount: global.autocadEvents.all.length + }); + } + + // Add timestamp if not provided + if (!eventData.timestamp) { + eventData.timestamp = new Date().toISOString(); + } + + // Store in global all events array + global.autocadEvents.all.push(eventData); + eventCount++; + + // Limit memory usage by removing old events if we exceed MAX_EVENTS_IN_MEMORY + if (global.autocadEvents.all.length > MAX_EVENTS_IN_MEMORY) { + const excess = global.autocadEvents.all.length - MAX_EVENTS_IN_MEMORY; + + // Remove oldest events + const removed = global.autocadEvents.all.splice(0, excess); + console.log(`Memory limit reached: Removed ${removed.length} old events`); + + // Also remove from specific event arrays + for (const category of Object.keys(global.autocadEvents.blockEvents)) { + if (global.autocadEvents.blockEvents[category].length > MAX_EVENTS_IN_MEMORY / 10) { + const categoryExcess = global.autocadEvents.blockEvents[category].length - (MAX_EVENTS_IN_MEMORY / 10); + global.autocadEvents.blockEvents[category].splice(0, categoryExcess); + } + } + + for (const category of Object.keys(global.autocadEvents.entityEvents)) { + if (global.autocadEvents.entityEvents[category].length > MAX_EVENTS_IN_MEMORY / 10) { + const categoryExcess = global.autocadEvents.entityEvents[category].length - (MAX_EVENTS_IN_MEMORY / 10); + global.autocadEvents.entityEvents[category].splice(0, categoryExcess); + } + } + } + + // Categorize by event type + const eventType = eventData.EventType; + + // Handle block-specific events + if (eventType.startsWith('Block')) { + switch (eventType) { + case 'BlockAdded': + global.autocadEvents.blockEvents.added.push(eventData); + break; + case 'BlockErased': + global.autocadEvents.blockEvents.erased.push(eventData); + break; + case 'BlockStretched': + global.autocadEvents.blockEvents.stretched.push(eventData); + break; + case 'BlockRotated': + global.autocadEvents.blockEvents.rotated.push(eventData); + break; + case 'BlockScaled': + global.autocadEvents.blockEvents.scaled.push(eventData); + break; + case 'BlockMoved': + global.autocadEvents.blockEvents.moved.push(eventData); + break; + case 'BlockAttributesChanged': + global.autocadEvents.blockEvents.attributesChanged.push(eventData); + break; + case 'BlockModified': + global.autocadEvents.blockEvents.modified.push(eventData); + break; + } + } else { + // Handle non-block entity events + switch (eventType) { + case 'Added': + global.autocadEvents.entityEvents.added.push(eventData); + break; + case 'Erased': + global.autocadEvents.entityEvents.erased.push(eventData); + break; + case 'Stretched': + global.autocadEvents.entityEvents.stretched.push(eventData); + break; + case 'Modified': + global.autocadEvents.entityEvents.modified.push(eventData); + break; + } + } + + // Notify all connected clients of the new event + notifyClientsOfNewEvent(eventData); + + // Periodically save events to disk (every 20 events or 5 minutes) + const currentTime = Date.now(); + if (eventCount % 20 === 0 || (currentTime - lastSaveTime) > 300000) { + saveEventsToDisk(); + lastSaveTime = currentTime; + } + + // Enhanced logging based on event type + if (eventType.startsWith('Block')) { + // Basic log message with event type and block name + console.log(`Received ${eventType} event for ${eventData.BlockName}`); + + // Use the formatting function for detailed output + if (process.env.VERBOSE_LOGGING === 'true') { + console.log(formatBlockData(eventData)); + } + + // Add specific position change logging + if (eventType === 'BlockMoved' && eventData.OldPosition && eventData.NewPosition) { + console.log(`POSITION CHANGE: From (${eventData.OldPosition.X.toFixed(2)}, ${eventData.OldPosition.Y.toFixed(2)}, ${eventData.OldPosition.Z.toFixed(2)}) to (${eventData.NewPosition.X.toFixed(2)}, ${eventData.NewPosition.Y.toFixed(2)}, ${eventData.NewPosition.Z.toFixed(2)})`); + } + } else { + // Standard logging for non-block events + console.log(`Received ${eventType} event for ${eventData.ObjectType}`); + } + + res.status(200).json({ success: true, message: 'Event received' }); + } catch (error) { + console.error('Error processing event:', error); + res.status(500).json({ success: false, message: 'Error processing event' }); + } +}); + +// Add endpoints to get events by type +app.get('/api/events', (req, res) => { + res.json(global.autocadEvents.all); +}); + +app.get('/api/events/blocks', (req, res) => { + res.json(global.autocadEvents.blockEvents); +}); + +app.get('/api/events/blocks/:type', (req, res) => { + const eventType = req.params.type; + if (global.autocadEvents.blockEvents[eventType]) { + res.json(global.autocadEvents.blockEvents[eventType]); + } else { + res.status(404).json({ message: 'Event type not found' }); + } +}); + +app.get('/api/events/entities', (req, res) => { + res.json(global.autocadEvents.entityEvents); +}); + +app.get('/api/events/entities/:type', (req, res) => { + const eventType = req.params.type; + if (global.autocadEvents.entityEvents[eventType]) { + res.json(global.autocadEvents.entityEvents[eventType]); + } else { + res.status(404).json({ message: 'Event type not found' }); + } +}); + +// Add an endpoint to clear events +app.delete('/api/events', (req, res) => { + global.autocadEvents = { + all: [], + blockEvents: { + added: [], + erased: [], + stretched: [], + rotated: [], + scaled: [], + moved: [], + attributesChanged: [], + modified: [] + }, + entityEvents: { + added: [], + erased: [], + stretched: [], + modified: [] + } + }; + res.json({ success: true, message: 'All events cleared' }); +}); + +// Function to save events to disk +function saveEventsToDisk() { + try { + const timestamp = new Date().toISOString().replace(/:/g, '-'); + const filePath = path.join(dataDir, `events_${timestamp}.json`); + + // Use a more memory-efficient approach for large event arrays + if (global.autocadEvents.all.length > 1000) { + // Write to file stream instead of creating a large string in memory + const fileStream = fs.createWriteStream(filePath); + fileStream.write('{\n'); + fileStream.write('"all": [\n'); + + global.autocadEvents.all.forEach((event, index) => { + const eventJson = JSON.stringify(event); + fileStream.write(eventJson); + if (index < global.autocadEvents.all.length - 1) { + fileStream.write(',\n'); + } else { + fileStream.write('\n'); + } + }); + + fileStream.write('],\n'); + fileStream.write('"blockEvents": ' + JSON.stringify(global.autocadEvents.blockEvents) + ',\n'); + fileStream.write('"entityEvents": ' + JSON.stringify(global.autocadEvents.entityEvents) + '\n'); + fileStream.write('}'); + fileStream.end(); + + console.log(`Events saved to ${filePath} (stream method)`); + } else { + // For smaller datasets, use the simpler approach + fs.writeFileSync(filePath, JSON.stringify(global.autocadEvents, null, 2)); + console.log(`Events saved to ${filePath}`); + } + } catch (error) { + console.error('Error saving events to disk:', error); + } +} + +// Connect to MongoDB (optional - comment out if you don't want to use it) +// mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/autocad_tracker') +// .then(() => console.log('Connected to MongoDB')) +// .catch(err => console.error('Could not connect to MongoDB', err)); + +// Default route +app.get('/', (req, res) => { + res.redirect('/index.html'); +}); + +// Legacy route for backward compatibility +app.get('/attribute-history', (req, res) => { + res.redirect('/index.html'); +}); + +// Update the formatBlockData function for better color formatting +function formatBlockData(block) { + let output = ''; + + // Add basic block info + output += `\n--- Block Details ---`; + output += `\nName: ${block.BlockName}`; + output += `\nObject ID: ${block.ObjectId}`; + + // Add position information + if (block.NewPosition) { + output += `\nPosition: (${block.NewPosition.X.toFixed(2)}, ${block.NewPosition.Y.toFixed(2)}, ${block.NewPosition.Z.toFixed(2)})`; + + // Add old position if available (for moved blocks) + if (block.OldPosition) { + output += `\nOld Position: (${block.OldPosition.X.toFixed(2)}, ${block.OldPosition.Y.toFixed(2)}, ${block.OldPosition.Z.toFixed(2)})`; + } + } else if (block.Position) { + output += `\nPosition: (${block.Position.X.toFixed(2)}, ${block.Position.Y.toFixed(2)}, ${block.Position.Z.toFixed(2)})`; + } + + // Add rotation information + if (block.NewRotation !== undefined) { + output += `\nRotation: ${(block.NewRotation * 180 / Math.PI).toFixed(2)} degrees`; + + // Add old rotation if available + if (block.OldRotation !== undefined) { + output += `\nOld Rotation: ${(block.OldRotation * 180 / Math.PI).toFixed(2)} degrees`; + } + } + + // Add scale information + if (block.NewScale) { + output += `\nScale: X=${block.NewScale.X.toFixed(3)}, Y=${block.NewScale.Y.toFixed(3)}, Z=${block.NewScale.Z.toFixed(3)}`; + + // Add old scale if available + if (block.OldScale) { + output += `\nOld Scale: X=${block.OldScale.X.toFixed(3)}, Y=${block.OldScale.Y.toFixed(3)}, Z=${block.OldScale.Z.toFixed(3)}`; + } + } + + // Add attribute information + if (block.Attributes && Object.keys(block.Attributes).length > 0) { + output += `\nAttributes:`; + for (const [key, value] of Object.entries(block.Attributes)) { + output += `\n ${key}: ${value}`; + } + } else { + output += `\nNo attributes`; + } + + // Add attribute changes if available + if (block.AttributeChanges && Object.keys(block.AttributeChanges).length > 0) { + output += `\nAttribute Changes:`; + for (const [key, change] of Object.entries(block.AttributeChanges)) { + output += `\n ${key}: "${change.OldValue}" -> "${change.NewValue}"`; + } + } + + return output; +} + +// Add an endpoint to view detailed block information +app.get('/api/events/blocks/details/:id', (req, res) => { + const blockId = req.params.id; + + // Search for the block in all categories + const allBlocks = Object.values(global.autocadEvents.blockEvents) + .flat() + .filter(event => event.ObjectId === blockId); + + if (allBlocks.length === 0) { + return res.status(404).json({ message: 'Block not found' }); + } + + // Get the latest block data + const latestBlock = allBlocks[allBlocks.length - 1]; + + // Format the block data + const formattedData = { + blockData: latestBlock, + formatted: formatBlockData(latestBlock), + history: allBlocks.map(block => ({ + timestamp: block.Timestamp, + eventType: block.EventType + })) + }; + + res.json(formattedData); +}); + +// Add an endpoint to get a list of all blocks with their latest attributes +app.get('/api/blocks', (req, res) => { + // Get all block events + const allBlockEvents = global.autocadEvents.all.filter(event => + event.EventType && event.EventType.startsWith('Block') + ); + + // Create a map to store the latest state of each block + const blockMap = new Map(); + + // Process events to get the latest state of each block + allBlockEvents.forEach(event => { + // Use object ID as the unique identifier + const blockId = event.ObjectId; + + // Update the map with the latest event for each block + blockMap.set(blockId, event); + }); + + // Convert map to array of blocks + const blocks = Array.from(blockMap.values()).map(block => ({ + id: block.ObjectId, + name: block.BlockName, + position: block.NewPosition || block.Position, + attributes: block.Attributes || {}, + lastEventType: block.EventType, + timestamp: block.Timestamp + })); + + res.json(blocks); +}); + +// Add an endpoint to get all attribute changes +app.get('/api/blocks/attribute-changes', (req, res) => { + const attributeChanges = global.autocadEvents.blockEvents.attributesChanged; + + // Process attribute changes to make them more meaningful + const processedChanges = attributeChanges.map(event => { + // Extract basic information + const { BlockName, ObjectId, Timestamp, Attributes, AttributeChanges } = event; + + // Process each changed attribute + const changes = []; + if (AttributeChanges) { + for (const [tag, change] of Object.entries(AttributeChanges)) { + changes.push({ + tag, + oldValue: change.OldValue, + newValue: change.NewValue, + changeTime: Timestamp + }); + } + } + + return { + blockName: BlockName, + objectId: ObjectId, + timestamp: Timestamp, + currentAttributes: Attributes || {}, + changes + }; + }); + + res.json(processedChanges); +}); + +// Add an endpoint to get attribute change history for a specific block +app.get('/api/blocks/:id/attribute-history', (req, res) => { + const blockId = req.params.id; + + // Find all attribute change events for this block + const attributeChanges = global.autocadEvents.all + .filter(event => + event.ObjectId === blockId && + (event.AttributeChanges || event.EventType === 'BlockAttributesChanged') + ) + .sort((a, b) => new Date(a.Timestamp) - new Date(b.Timestamp)); + + if (attributeChanges.length === 0) { + return res.status(404).json({ message: 'No attribute changes found for this block' }); + } + + // Process the attribute history + const history = []; + const latestAttributes = {}; + + attributeChanges.forEach(event => { + const timestamp = event.Timestamp; + + // Track changes for each attribute + if (event.AttributeChanges) { + for (const [tag, change] of Object.entries(event.AttributeChanges)) { + history.push({ + tag, + oldValue: change.OldValue, + newValue: change.NewValue, + timestamp, + blockName: event.BlockName + }); + + // Update latest attribute value + latestAttributes[tag] = change.NewValue; + } + } + + // Also get initial values from Attributes field if present + if (event.Attributes && Object.keys(event.Attributes).length > 0) { + for (const [tag, value] of Object.entries(event.Attributes)) { + if (!latestAttributes[tag]) { + latestAttributes[tag] = value; + } + } + } + }); + + // Return formatted response + res.json({ + blockId, + blockName: attributeChanges[0].BlockName, + latestAttributes, + history + }); +}); + +// Add an endpoint to get position change history for a specific block +app.get('/api/blocks/:id/position-history', (req, res) => { + const blockId = req.params.id; + + // Find all position change events for this block + const positionChanges = global.autocadEvents.all + .filter(event => + event.ObjectId === blockId && + (event.EventType === 'BlockMoved' || + event.EventType === 'BlockModified' || + event.EventType === 'BlockAdded') && + (event.NewPosition || event.Position) + ) + .sort((a, b) => new Date(a.Timestamp) - new Date(b.Timestamp)); + + if (positionChanges.length === 0) { + return res.status(404).json({ message: 'No position changes found for this block' }); + } + + // Process the position history + const history = positionChanges.map(event => { + const entry = { + timestamp: event.Timestamp, + eventType: event.EventType, + blockName: event.BlockName + }; + + // Add position data + if (event.NewPosition) { + entry.position = { + x: event.NewPosition.X, + y: event.NewPosition.Y, + z: event.NewPosition.Z + }; + + // Add old position if available + if (event.OldPosition) { + entry.oldPosition = { + x: event.OldPosition.X, + y: event.OldPosition.Y, + z: event.OldPosition.Z + }; + } + } else if (event.Position) { + entry.position = { + x: event.Position.X, + y: event.Position.Y, + z: event.Position.Z + }; + } + + return entry; + }); + + // Get latest position + const latestEvent = positionChanges[positionChanges.length - 1]; + const latestPosition = latestEvent.NewPosition || latestEvent.Position; + + // Return formatted response + res.json({ + blockId, + blockName: positionChanges[0].BlockName, + latestPosition: { + x: latestPosition.X, + y: latestPosition.Y, + z: latestPosition.Z + }, + history + }); +}); + +// Add an endpoint to get server status and performance metrics +app.get('/api/status', (req, res) => { + const status = { + uptime: process.uptime(), + memoryUsage: process.memoryUsage(), + eventsCount: { + total: global.autocadEvents.all.length, + blocks: Object.keys(global.autocadEvents.blockEvents).reduce( + (total, key) => total + global.autocadEvents.blockEvents[key].length, 0 + ), + entities: Object.keys(global.autocadEvents.entityEvents).reduce( + (total, key) => total + global.autocadEvents.entityEvents[key].length, 0 + ) + }, + sseClients: sseClients.length, + lastSaveTime: new Date(lastSaveTime).toISOString() + }; + + res.json(status); +}); + +// Add an endpoint for checking server status (lightweight compared to SSE) +app.get('/api/server-status', (req, res) => { + res.json({ + status: 'online', + timestamp: new Date().toISOString(), + uptime: process.uptime() + }); +}); + +// Start server +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); + console.log(`API endpoints available at:`); + console.log(` - GET /api/events: Get all events`); + console.log(` - GET /api/events/blocks: Get all block events`); + console.log(` - GET /api/events/blocks/:type: Get block events by type`); + console.log(` - GET /api/events/entities: Get all entity events`); + console.log(` - GET /api/events/entities/:type: Get entity events by type`); + console.log(` - DELETE /api/events: Clear all events`); + console.log(` - GET /api/events/blocks/details/:id: Get detailed block information`); + console.log(` - GET /api/blocks: Get a list of all blocks with their latest attributes`); + console.log(` - GET /api/blocks/attribute-changes: Get all attribute changes`); + console.log(` - GET /api/blocks/:id/attribute-history: Get attribute change history for a specific block`); + console.log(` - GET /api/blocks/:id/position-history: Get position change history for a specific block`); + console.log(` - GET /api/status: Get server status and performance metrics`); + console.log(` - GET /api/server-status: Get server status (lightweight)`); + console.log(` - Front-end interface available at: http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/test-client.js b/test-client.js new file mode 100644 index 0000000..9bca625 --- /dev/null +++ b/test-client.js @@ -0,0 +1,63 @@ +const fetch = require('node-fetch'); + +// Simulate sending AutoCAD events to our server +async function sendTestEvent() { + try { + // Sample data for an Added event + const addedEvent = { + EventType: "Added", + ObjectId: "A1B2C3D4", + ObjectType: "Line", + Position: { X: 100.5, Y: 200.8, Z: 0.0 }, + Timestamp: new Date().toISOString() + }; + + // Send to server + console.log('Sending test data to server...'); + const response = await fetch('http://localhost:3000/api/autocad-events', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(addedEvent), + }); + + const data = await response.json(); + console.log('Server response:', data); + + // Sample data for a Modified event + const modifiedEvent = { + EventType: "Modified", + ObjectId: "A1B2C3D4", + ObjectType: "Line", + OldPosition: { X: 100.5, Y: 200.8, Z: 0.0 }, + NewPosition: { X: 150.2, Y: 250.3, Z: 0.0 }, + Timestamp: new Date().toISOString() + }; + + // Send to server + console.log('Sending modification test data to server...'); + const modResponse = await fetch('http://localhost:3000/api/autocad-events', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(modifiedEvent), + }); + + const modData = await modResponse.json(); + console.log('Server response:', modData); + + // Retrieve all events + console.log('Retrieving all events from server...'); + const getResponse = await fetch('http://localhost:3000/api/autocad-events'); + const allEvents = await getResponse.json(); + console.log('All stored events:', JSON.stringify(allEvents, null, 2)); + + } catch (error) { + console.error('Error sending test data:', error); + } +} + +// Run the test +sendTestEvent(); \ No newline at end of file