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}`); });