AutoCadServer/server.js
2025-05-16 18:17:54 +04:00

712 lines
22 KiB
JavaScript

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