cyclecad 2.1.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
  2. package/BILLING-INDEX.md +293 -0
  3. package/BILLING-INTEGRATION-GUIDE.md +414 -0
  4. package/COLLABORATION-INDEX.md +440 -0
  5. package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
  6. package/DELIVERABLES.txt +296 -445
  7. package/DOCKER-BUILD-MANIFEST.txt +483 -0
  8. package/DOCKER-FILES-REFERENCE.md +440 -0
  9. package/DOCKER-INFRASTRUCTURE.md +475 -0
  10. package/DOCKER-README.md +435 -0
  11. package/Dockerfile +33 -55
  12. package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
  13. package/ENHANCEMENT_SUMMARY.txt +308 -0
  14. package/FEATURE_INVENTORY.md +235 -0
  15. package/FUSION360_FEATURES_SUMMARY.md +452 -0
  16. package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
  17. package/FUSION360_PARITY_SUMMARY.md +520 -0
  18. package/FUSION360_QUICK_REFERENCE.md +351 -0
  19. package/MODULE_API_REFERENCE.md +712 -0
  20. package/MODULE_INVENTORY.txt +264 -0
  21. package/PWA-FILES-CREATED.txt +350 -0
  22. package/QUICK-START-TESTING.md +126 -0
  23. package/STEP-IMPORT-QUICKSTART.md +347 -0
  24. package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
  25. package/app/css/mobile.css +1074 -0
  26. package/app/icons/generate-icons.js +203 -0
  27. package/app/index.html +1342 -5031
  28. package/app/js/app.js +1312 -514
  29. package/app/js/billing-ui.js +990 -0
  30. package/app/js/brep-kernel.js +933 -981
  31. package/app/js/collab-client.js +750 -0
  32. package/app/js/mobile-nav.js +623 -0
  33. package/app/js/mobile-toolbar.js +476 -0
  34. package/app/js/modules/animation-module.js +497 -3
  35. package/app/js/modules/billing-module.js +724 -0
  36. package/app/js/modules/cam-module.js +507 -2
  37. package/app/js/modules/collaboration-module.js +513 -0
  38. package/app/js/modules/constraint-module.js +1266 -0
  39. package/app/js/modules/data-module.js +544 -1146
  40. package/app/js/modules/formats-module.js +438 -738
  41. package/app/js/modules/inspection-module.js +393 -0
  42. package/app/js/modules/mesh-module-enhanced.js +880 -0
  43. package/app/js/modules/plugin-module.js +597 -0
  44. package/app/js/modules/rendering-module.js +460 -0
  45. package/app/js/modules/scripting-module.js +593 -475
  46. package/app/js/modules/sketch-module.js +998 -2
  47. package/app/js/modules/step-module-enhanced.js +938 -0
  48. package/app/js/modules/surface-module.js +312 -0
  49. package/app/js/modules/version-module.js +420 -0
  50. package/app/js/offline-manager.js +705 -0
  51. package/app/js/responsive-init.js +360 -0
  52. package/app/js/touch-handler.js +429 -0
  53. package/app/manifest.json +211 -0
  54. package/app/offline.html +508 -0
  55. package/app/sw.js +571 -0
  56. package/app/tests/billing-tests.html +779 -0
  57. package/app/tests/brep-tests.html +980 -0
  58. package/app/tests/collab-tests.html +743 -0
  59. package/app/tests/mobile-tests.html +1299 -0
  60. package/app/tests/pwa-tests.html +1134 -0
  61. package/app/tests/step-tests.html +1042 -0
  62. package/app/tests/test-agent-v3.html +719 -0
  63. package/cycleCAD-Architecture-v2.pptx +0 -0
  64. package/docker-compose.yml +225 -0
  65. package/docs/BILLING-HELP.json +260 -0
  66. package/docs/BILLING-README.md +639 -0
  67. package/docs/BILLING-TUTORIAL.md +736 -0
  68. package/docs/BREP-HELP.json +326 -0
  69. package/docs/BREP-TUTORIAL.md +802 -0
  70. package/docs/COLLABORATION-HELP.json +228 -0
  71. package/docs/COLLABORATION-TUTORIAL.md +818 -0
  72. package/docs/DOCKER-HELP.json +224 -0
  73. package/docs/DOCKER-TUTORIAL.md +974 -0
  74. package/docs/MOBILE-HELP.json +243 -0
  75. package/docs/MOBILE-RESPONSIVE-README.md +378 -0
  76. package/docs/MOBILE-TUTORIAL.md +747 -0
  77. package/docs/PWA-HELP.json +228 -0
  78. package/docs/PWA-README.md +662 -0
  79. package/docs/PWA-TUTORIAL.md +757 -0
  80. package/docs/STEP-HELP.json +481 -0
  81. package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
  82. package/docs/TESTING-GUIDE.md +528 -0
  83. package/docs/TESTING-HELP.json +182 -0
  84. package/fusion-vs-cyclecad.html +1771 -0
  85. package/nginx.conf +237 -0
  86. package/package.json +1 -1
  87. package/server/Dockerfile.converter +51 -0
  88. package/server/Dockerfile.signaling +28 -0
  89. package/server/billing-server.js +487 -0
  90. package/server/converter-enhanced.py +528 -0
  91. package/server/requirements-converter.txt +29 -0
  92. package/server/signaling-server.js +801 -0
  93. package/tests/docker-tests.sh +389 -0
  94. package/~$cycleCAD-Architecture-v2.pptx +0 -0
@@ -0,0 +1,801 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cycleCAD WebSocket Signaling Server
4
+ * Real-time collaboration via WebRTC signaling, presence, and CRDT operation relay
5
+ *
6
+ * Features:
7
+ * - Room management (create, join, leave, list)
8
+ * - WebRTC signaling (offer, answer, ICE candidates)
9
+ * - User presence (cursor position, selection, online status)
10
+ * - CRDT operation relay (broadcast ops to all peers in room)
11
+ * - Chat messages with timestamps
12
+ * - Room capacity limits (max 10 users per room)
13
+ * - Heartbeat/ping-pong for connection health
14
+ * - Graceful reconnection with state recovery
15
+ * - Rate limiting (100 messages/sec per client)
16
+ * - Room state persistence (auto-save to disk)
17
+ */
18
+
19
+ const WebSocket = require('ws');
20
+ const express = require('express');
21
+ const http = require('http');
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const crypto = require('crypto');
25
+
26
+ const PORT = process.env.PORT || 8788;
27
+ const MAX_USERS_PER_ROOM = 10;
28
+ const HEARTBEAT_INTERVAL = 30000; // 30 seconds
29
+ const RATE_LIMIT_WINDOW = 1000; // 1 second
30
+ const RATE_LIMIT_MAX = 100; // messages per second
31
+ const STATE_SAVE_INTERVAL = 5000; // auto-save every 5 seconds
32
+ const STATE_FILE = path.join(__dirname, 'rooms-state.json');
33
+
34
+ // Data structures
35
+ const rooms = new Map(); // roomId -> Room object
36
+ const clients = new Map(); // clientId -> Client object
37
+ let clientCounter = 0;
38
+
39
+ // Express app for HTTP endpoints
40
+ const app = express();
41
+ app.use(express.json());
42
+
43
+ // Create HTTP server
44
+ const server = http.createServer(app);
45
+
46
+ // Create WebSocket server
47
+ const wss = new WebSocket.Server({ server });
48
+
49
+ // ========== Room Management ==========
50
+
51
+ class Room {
52
+ constructor(roomId, options = {}) {
53
+ this.id = roomId;
54
+ this.name = options.name || `Room ${roomId}`;
55
+ this.password = options.password || null;
56
+ this.users = new Map(); // userId -> User object
57
+ this.operations = []; // CRDT operations log
58
+ this.chatHistory = []; // chat messages
59
+ this.createdAt = new Date();
60
+ this.maxUsers = options.maxUsers || MAX_USERS_PER_ROOM;
61
+ this.isPrivate = !!options.password;
62
+ }
63
+
64
+ addUser(userId, user) {
65
+ if (this.users.size >= this.maxUsers) {
66
+ throw new Error(`Room ${this.id} is full (max ${this.maxUsers} users)`);
67
+ }
68
+ this.users.set(userId, user);
69
+ return user;
70
+ }
71
+
72
+ removeUser(userId) {
73
+ this.users.delete(userId);
74
+ }
75
+
76
+ getUser(userId) {
77
+ return this.users.get(userId);
78
+ }
79
+
80
+ getUsers() {
81
+ return Array.from(this.users.values());
82
+ }
83
+
84
+ broadcast(message, excludeUserId = null) {
85
+ this.users.forEach((user, userId) => {
86
+ if (excludeUserId && userId === excludeUserId) return;
87
+ user.send(message);
88
+ });
89
+ }
90
+
91
+ toJSON() {
92
+ return {
93
+ id: this.id,
94
+ name: this.name,
95
+ userCount: this.users.size,
96
+ maxUsers: this.maxUsers,
97
+ isPrivate: this.isPrivate,
98
+ createdAt: this.createdAt,
99
+ users: this.getUsers().map(u => u.toJSON()),
100
+ operationCount: this.operations.length,
101
+ chatCount: this.chatHistory.length
102
+ };
103
+ }
104
+ }
105
+
106
+ class User {
107
+ constructor(userId, clientId, name = `User ${userId}`) {
108
+ this.id = userId;
109
+ this.clientId = clientId;
110
+ this.name = name;
111
+ this.cursor = { x: 0, y: 0 };
112
+ this.selection = [];
113
+ this.color = generateUserColor();
114
+ this.status = 'online';
115
+ this.joinedAt = new Date();
116
+ this.lastSeen = new Date();
117
+ this.messageCount = 0;
118
+ }
119
+
120
+ send(message) {
121
+ const client = clients.get(this.clientId);
122
+ if (client && client.ws.readyState === WebSocket.OPEN) {
123
+ client.ws.send(JSON.stringify(message));
124
+ }
125
+ }
126
+
127
+ toJSON() {
128
+ return {
129
+ id: this.id,
130
+ name: this.name,
131
+ color: this.color,
132
+ cursor: this.cursor,
133
+ selection: this.selection,
134
+ status: this.status,
135
+ joinedAt: this.joinedAt
136
+ };
137
+ }
138
+ }
139
+
140
+ class Client {
141
+ constructor(ws, clientId) {
142
+ this.ws = ws;
143
+ this.id = clientId;
144
+ this.userId = null;
145
+ this.roomId = null;
146
+ this.messageCount = 0;
147
+ this.messageWindowStart = Date.now();
148
+ this.isAlive = true;
149
+ this.lastHeartbeat = Date.now();
150
+ }
151
+
152
+ checkRateLimit() {
153
+ const now = Date.now();
154
+ if (now - this.messageWindowStart > RATE_LIMIT_WINDOW) {
155
+ this.messageCount = 0;
156
+ this.messageWindowStart = now;
157
+ }
158
+ return this.messageCount++ < RATE_LIMIT_MAX;
159
+ }
160
+
161
+ toJSON() {
162
+ return {
163
+ id: this.id,
164
+ userId: this.userId,
165
+ roomId: this.roomId,
166
+ lastHeartbeat: this.lastHeartbeat
167
+ };
168
+ }
169
+ }
170
+
171
+ // ========== WebSocket Handlers ==========
172
+
173
+ wss.on('connection', (ws) => {
174
+ const clientId = `client-${++clientCounter}`;
175
+ const client = new Client(ws, clientId);
176
+ clients.set(clientId, client);
177
+
178
+ console.log(`[${new Date().toISOString()}] Client connected: ${clientId}`);
179
+
180
+ // Send welcome message
181
+ ws.send(JSON.stringify({
182
+ type: 'welcome',
183
+ clientId,
184
+ timestamp: new Date().toISOString(),
185
+ version: '1.0.0'
186
+ }));
187
+
188
+ // Handle incoming messages
189
+ ws.on('message', (data) => {
190
+ try {
191
+ const message = JSON.parse(data);
192
+ handleMessage(client, message);
193
+ } catch (error) {
194
+ console.error(`[${new Date().toISOString()}] Parse error from ${clientId}:`, error.message);
195
+ ws.send(JSON.stringify({
196
+ type: 'error',
197
+ message: 'Invalid message format',
198
+ timestamp: new Date().toISOString()
199
+ }));
200
+ }
201
+ });
202
+
203
+ // Handle pong (heartbeat response)
204
+ ws.on('pong', () => {
205
+ client.isAlive = true;
206
+ client.lastHeartbeat = Date.now();
207
+ });
208
+
209
+ // Handle client disconnect
210
+ ws.on('close', () => {
211
+ handleClientDisconnect(client);
212
+ });
213
+
214
+ // Handle errors
215
+ ws.on('error', (error) => {
216
+ console.error(`[${new Date().toISOString()}] WebSocket error from ${clientId}:`, error.message);
217
+ });
218
+ });
219
+
220
+ // ========== Message Handler ==========
221
+
222
+ function handleMessage(client, message) {
223
+ const { type, payload } = message;
224
+
225
+ // Rate limiting
226
+ if (!client.checkRateLimit()) {
227
+ client.ws.send(JSON.stringify({
228
+ type: 'error',
229
+ message: 'Rate limit exceeded',
230
+ timestamp: new Date().toISOString()
231
+ }));
232
+ return;
233
+ }
234
+
235
+ console.log(`[${new Date().toISOString()}] Message from ${client.id}: ${type}`);
236
+
237
+ switch (type) {
238
+ case 'join-room':
239
+ handleJoinRoom(client, payload);
240
+ break;
241
+
242
+ case 'create-room':
243
+ handleCreateRoom(client, payload);
244
+ break;
245
+
246
+ case 'leave-room':
247
+ handleLeaveRoom(client);
248
+ break;
249
+
250
+ case 'signaling-offer':
251
+ handleSignalingOffer(client, payload);
252
+ break;
253
+
254
+ case 'signaling-answer':
255
+ handleSignalingAnswer(client, payload);
256
+ break;
257
+
258
+ case 'ice-candidate':
259
+ handleIceCandidate(client, payload);
260
+ break;
261
+
262
+ case 'cursor-update':
263
+ handleCursorUpdate(client, payload);
264
+ break;
265
+
266
+ case 'selection-update':
267
+ handleSelectionUpdate(client, payload);
268
+ break;
269
+
270
+ case 'operation':
271
+ handleOperation(client, payload);
272
+ break;
273
+
274
+ case 'chat-message':
275
+ handleChatMessage(client, payload);
276
+ break;
277
+
278
+ case 'user-status':
279
+ handleUserStatus(client, payload);
280
+ break;
281
+
282
+ case 'ping':
283
+ client.ws.send(JSON.stringify({
284
+ type: 'pong',
285
+ timestamp: new Date().toISOString()
286
+ }));
287
+ break;
288
+
289
+ default:
290
+ console.warn(`[${new Date().toISOString()}] Unknown message type: ${type}`);
291
+ }
292
+ }
293
+
294
+ // ========== Room Management Handlers ==========
295
+
296
+ function handleCreateRoom(client, payload) {
297
+ const { roomId, name, password, maxUsers } = payload;
298
+
299
+ if (!roomId || typeof roomId !== 'string') {
300
+ client.ws.send(JSON.stringify({
301
+ type: 'error',
302
+ message: 'Invalid roomId',
303
+ timestamp: new Date().toISOString()
304
+ }));
305
+ return;
306
+ }
307
+
308
+ if (rooms.has(roomId)) {
309
+ client.ws.send(JSON.stringify({
310
+ type: 'error',
311
+ message: 'Room already exists',
312
+ timestamp: new Date().toISOString()
313
+ }));
314
+ return;
315
+ }
316
+
317
+ const room = new Room(roomId, { name, password, maxUsers });
318
+ rooms.set(roomId, room);
319
+
320
+ client.ws.send(JSON.stringify({
321
+ type: 'room-created',
322
+ roomId,
323
+ room: room.toJSON(),
324
+ timestamp: new Date().toISOString()
325
+ }));
326
+
327
+ console.log(`[${new Date().toISOString()}] Room created: ${roomId}`);
328
+ }
329
+
330
+ function handleJoinRoom(client, payload) {
331
+ const { roomId, userId, userName, password } = payload;
332
+
333
+ if (!roomId || !userId) {
334
+ client.ws.send(JSON.stringify({
335
+ type: 'error',
336
+ message: 'Invalid roomId or userId',
337
+ timestamp: new Date().toISOString()
338
+ }));
339
+ return;
340
+ }
341
+
342
+ let room = rooms.get(roomId);
343
+
344
+ // Auto-create room if it doesn't exist
345
+ if (!room) {
346
+ room = new Room(roomId, { name: `Room ${roomId}` });
347
+ rooms.set(roomId, room);
348
+ }
349
+
350
+ // Check password if required
351
+ if (room.password && room.password !== password) {
352
+ client.ws.send(JSON.stringify({
353
+ type: 'error',
354
+ message: 'Invalid room password',
355
+ timestamp: new Date().toISOString()
356
+ }));
357
+ return;
358
+ }
359
+
360
+ // Check capacity
361
+ if (room.users.size >= room.maxUsers) {
362
+ client.ws.send(JSON.stringify({
363
+ type: 'error',
364
+ message: `Room is full (max ${room.maxUsers} users)`,
365
+ timestamp: new Date().toISOString()
366
+ }));
367
+ return;
368
+ }
369
+
370
+ // Leave previous room if in one
371
+ if (client.roomId) {
372
+ handleLeaveRoom(client);
373
+ }
374
+
375
+ // Create user and add to room
376
+ const user = new User(userId, client.id, userName);
377
+ room.addUser(userId, user);
378
+ client.userId = userId;
379
+ client.roomId = roomId;
380
+
381
+ // Send confirmation to joining user
382
+ client.ws.send(JSON.stringify({
383
+ type: 'room-joined',
384
+ roomId,
385
+ userId,
386
+ room: room.toJSON(),
387
+ operations: room.operations.slice(-100), // Send last 100 ops for sync
388
+ chatHistory: room.chatHistory.slice(-50), // Send last 50 messages
389
+ timestamp: new Date().toISOString()
390
+ }));
391
+
392
+ // Broadcast user joined to room
393
+ room.broadcast({
394
+ type: 'user-joined',
395
+ user: user.toJSON(),
396
+ userCount: room.users.size,
397
+ timestamp: new Date().toISOString()
398
+ }, userId);
399
+
400
+ console.log(`[${new Date().toISOString()}] User ${userId} joined room ${roomId}`);
401
+ }
402
+
403
+ function handleLeaveRoom(client) {
404
+ if (!client.roomId) return;
405
+
406
+ const room = rooms.get(client.roomId);
407
+ if (!room) return;
408
+
409
+ const userId = client.userId;
410
+ room.removeUser(userId);
411
+
412
+ // Broadcast user left
413
+ room.broadcast({
414
+ type: 'user-left',
415
+ userId,
416
+ userCount: room.users.size,
417
+ timestamp: new Date().toISOString()
418
+ });
419
+
420
+ // Delete empty rooms after 5 minutes
421
+ if (room.users.size === 0) {
422
+ setTimeout(() => {
423
+ if (room.users.size === 0) {
424
+ rooms.delete(client.roomId);
425
+ console.log(`[${new Date().toISOString()}] Room deleted: ${client.roomId}`);
426
+ }
427
+ }, 300000);
428
+ }
429
+
430
+ client.roomId = null;
431
+ client.userId = null;
432
+
433
+ client.ws.send(JSON.stringify({
434
+ type: 'room-left',
435
+ timestamp: new Date().toISOString()
436
+ }));
437
+
438
+ console.log(`[${new Date().toISOString()}] User ${userId} left room ${room.id}`);
439
+ }
440
+
441
+ // ========== Real-time Handlers ==========
442
+
443
+ function handleSignalingOffer(client, payload) {
444
+ const { targetUserId, offer } = payload;
445
+ const room = rooms.get(client.roomId);
446
+ if (!room) return;
447
+
448
+ const targetUser = room.getUser(targetUserId);
449
+ if (!targetUser) return;
450
+
451
+ targetUser.send({
452
+ type: 'signaling-offer',
453
+ fromUserId: client.userId,
454
+ offer,
455
+ timestamp: new Date().toISOString()
456
+ });
457
+ }
458
+
459
+ function handleSignalingAnswer(client, payload) {
460
+ const { targetUserId, answer } = payload;
461
+ const room = rooms.get(client.roomId);
462
+ if (!room) return;
463
+
464
+ const targetUser = room.getUser(targetUserId);
465
+ if (!targetUser) return;
466
+
467
+ targetUser.send({
468
+ type: 'signaling-answer',
469
+ fromUserId: client.userId,
470
+ answer,
471
+ timestamp: new Date().toISOString()
472
+ });
473
+ }
474
+
475
+ function handleIceCandidate(client, payload) {
476
+ const { targetUserId, candidate } = payload;
477
+ const room = rooms.get(client.roomId);
478
+ if (!room) return;
479
+
480
+ const targetUser = room.getUser(targetUserId);
481
+ if (!targetUser) return;
482
+
483
+ targetUser.send({
484
+ type: 'ice-candidate',
485
+ fromUserId: client.userId,
486
+ candidate,
487
+ timestamp: new Date().toISOString()
488
+ });
489
+ }
490
+
491
+ function handleCursorUpdate(client, payload) {
492
+ const { x, y } = payload;
493
+ const room = rooms.get(client.roomId);
494
+ if (!room) return;
495
+
496
+ const user = room.getUser(client.userId);
497
+ if (!user) return;
498
+
499
+ user.cursor = { x, y };
500
+
501
+ room.broadcast({
502
+ type: 'cursor-update',
503
+ userId: client.userId,
504
+ cursor: { x, y },
505
+ timestamp: new Date().toISOString()
506
+ }, client.userId);
507
+ }
508
+
509
+ function handleSelectionUpdate(client, payload) {
510
+ const { selection } = payload;
511
+ const room = rooms.get(client.roomId);
512
+ if (!room) return;
513
+
514
+ const user = room.getUser(client.userId);
515
+ if (!user) return;
516
+
517
+ user.selection = selection || [];
518
+
519
+ room.broadcast({
520
+ type: 'selection-update',
521
+ userId: client.userId,
522
+ selection: user.selection,
523
+ timestamp: new Date().toISOString()
524
+ }, client.userId);
525
+ }
526
+
527
+ function handleOperation(client, payload) {
528
+ const { op } = payload;
529
+ const room = rooms.get(client.roomId);
530
+ if (!room) return;
531
+
532
+ // Add operation to log
533
+ room.operations.push({
534
+ userId: client.userId,
535
+ op,
536
+ timestamp: new Date().toISOString()
537
+ });
538
+
539
+ // Broadcast to room
540
+ room.broadcast({
541
+ type: 'operation',
542
+ userId: client.userId,
543
+ op,
544
+ timestamp: new Date().toISOString()
545
+ }, client.userId);
546
+ }
547
+
548
+ function handleChatMessage(client, payload) {
549
+ const { text } = payload;
550
+ const room = rooms.get(client.roomId);
551
+ if (!room) return;
552
+
553
+ const user = room.getUser(client.userId);
554
+ if (!user) return;
555
+
556
+ user.messageCount++;
557
+
558
+ const message = {
559
+ userId: client.userId,
560
+ userName: user.name,
561
+ userColor: user.color,
562
+ text,
563
+ timestamp: new Date().toISOString()
564
+ };
565
+
566
+ room.chatHistory.push(message);
567
+
568
+ // Broadcast to room
569
+ room.broadcast({
570
+ type: 'chat-message',
571
+ ...message
572
+ });
573
+ }
574
+
575
+ function handleUserStatus(client, payload) {
576
+ const { status } = payload;
577
+ const room = rooms.get(client.roomId);
578
+ if (!room) return;
579
+
580
+ const user = room.getUser(client.userId);
581
+ if (!user) return;
582
+
583
+ user.status = status;
584
+ user.lastSeen = new Date();
585
+
586
+ room.broadcast({
587
+ type: 'user-status',
588
+ userId: client.userId,
589
+ status,
590
+ timestamp: new Date().toISOString()
591
+ });
592
+ }
593
+
594
+ function handleClientDisconnect(client) {
595
+ console.log(`[${new Date().toISOString()}] Client disconnected: ${client.id}`);
596
+
597
+ if (client.roomId) {
598
+ handleLeaveRoom(client);
599
+ }
600
+
601
+ clients.delete(client.id);
602
+ }
603
+
604
+ // ========== Heartbeat ==========
605
+
606
+ function startHeartbeat() {
607
+ setInterval(() => {
608
+ const now = Date.now();
609
+ wss.clients.forEach((ws) => {
610
+ const client = Array.from(clients.values()).find(c => c.ws === ws);
611
+ if (!client) return;
612
+
613
+ if (!client.isAlive) {
614
+ return ws.terminate();
615
+ }
616
+
617
+ client.isAlive = false;
618
+ ws.ping();
619
+ });
620
+ }, HEARTBEAT_INTERVAL);
621
+ }
622
+
623
+ // ========== State Persistence ==========
624
+
625
+ function saveRoomState() {
626
+ const state = {
627
+ timestamp: new Date().toISOString(),
628
+ rooms: Array.from(rooms.values()).map(room => ({
629
+ id: room.id,
630
+ name: room.name,
631
+ password: room.password,
632
+ maxUsers: room.maxUsers,
633
+ createdAt: room.createdAt,
634
+ users: room.getUsers().map(u => u.toJSON()),
635
+ operations: room.operations,
636
+ chatHistory: room.chatHistory
637
+ }))
638
+ };
639
+
640
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
641
+ }
642
+
643
+ function loadRoomState() {
644
+ if (!fs.existsSync(STATE_FILE)) return;
645
+
646
+ try {
647
+ const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
648
+ // Restore rooms without users (users must rejoin)
649
+ state.rooms.forEach(roomData => {
650
+ const room = new Room(roomData.id, {
651
+ name: roomData.name,
652
+ password: roomData.password,
653
+ maxUsers: roomData.maxUsers
654
+ });
655
+ room.operations = roomData.operations || [];
656
+ room.chatHistory = roomData.chatHistory || [];
657
+ rooms.set(room.id, room);
658
+ });
659
+ console.log(`[${new Date().toISOString()}] Loaded ${rooms.size} rooms from state file`);
660
+ } catch (error) {
661
+ console.error(`[${new Date().toISOString()}] Error loading room state:`, error.message);
662
+ }
663
+ }
664
+
665
+ setInterval(saveRoomState, STATE_SAVE_INTERVAL);
666
+
667
+ // ========== HTTP Endpoints ==========
668
+
669
+ app.get('/health', (req, res) => {
670
+ res.json({
671
+ status: 'healthy',
672
+ timestamp: new Date().toISOString(),
673
+ clients: clients.size,
674
+ rooms: rooms.size
675
+ });
676
+ });
677
+
678
+ app.get('/stats', (req, res) => {
679
+ res.json({
680
+ timestamp: new Date().toISOString(),
681
+ clients: clients.size,
682
+ rooms: rooms.size,
683
+ uptime: process.uptime(),
684
+ memory: process.memoryUsage()
685
+ });
686
+ });
687
+
688
+ app.get('/rooms', (req, res) => {
689
+ const roomList = Array.from(rooms.values()).map(room => room.toJSON());
690
+ res.json({
691
+ timestamp: new Date().toISOString(),
692
+ count: roomList.length,
693
+ rooms: roomList
694
+ });
695
+ });
696
+
697
+ app.get('/rooms/:roomId', (req, res) => {
698
+ const room = rooms.get(req.params.roomId);
699
+ if (!room) {
700
+ return res.status(404).json({ error: 'Room not found' });
701
+ }
702
+ res.json({
703
+ timestamp: new Date().toISOString(),
704
+ room: room.toJSON()
705
+ });
706
+ });
707
+
708
+ app.post('/rooms/:roomId/reset', (req, res) => {
709
+ const room = rooms.get(req.params.roomId);
710
+ if (!room) {
711
+ return res.status(404).json({ error: 'Room not found' });
712
+ }
713
+
714
+ // Notify users before reset
715
+ room.broadcast({
716
+ type: 'room-reset',
717
+ timestamp: new Date().toISOString()
718
+ });
719
+
720
+ room.operations = [];
721
+ room.chatHistory = [];
722
+
723
+ res.json({
724
+ timestamp: new Date().toISOString(),
725
+ message: 'Room reset successful'
726
+ });
727
+ });
728
+
729
+ app.post('/rooms/:roomId/close', (req, res) => {
730
+ const room = rooms.get(req.params.roomId);
731
+ if (!room) {
732
+ return res.status(404).json({ error: 'Room not found' });
733
+ }
734
+
735
+ room.broadcast({
736
+ type: 'room-closed',
737
+ timestamp: new Date().toISOString()
738
+ });
739
+
740
+ rooms.delete(req.params.roomId);
741
+
742
+ res.json({
743
+ timestamp: new Date().toISOString(),
744
+ message: 'Room closed'
745
+ });
746
+ });
747
+
748
+ // ========== Utility Functions ==========
749
+
750
+ function generateUserColor() {
751
+ const colors = [
752
+ '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8',
753
+ '#F7DC6F', '#BB8FCE', '#85C1E2', '#F8B195', '#D5A6BD'
754
+ ];
755
+ return colors[Math.floor(Math.random() * colors.length)];
756
+ }
757
+
758
+ // ========== Server Startup ==========
759
+
760
+ function start() {
761
+ loadRoomState();
762
+ startHeartbeat();
763
+
764
+ server.listen(PORT, () => {
765
+ console.log(`\n${'='.repeat(60)}`);
766
+ console.log(`cycleCAD WebSocket Signaling Server v1.0.0`);
767
+ console.log(`${'='.repeat(60)}`);
768
+ console.log(`Server listening on port ${PORT}`);
769
+ console.log(`WebSocket: ws://localhost:${PORT}`);
770
+ console.log(`HTTP Health: http://localhost:${PORT}/health`);
771
+ console.log(`HTTP Stats: http://localhost:${PORT}/stats`);
772
+ console.log(`HTTP Rooms: http://localhost:${PORT}/rooms`);
773
+ console.log(`${'='.repeat(60)}\n`);
774
+ });
775
+
776
+ // Graceful shutdown
777
+ process.on('SIGINT', () => {
778
+ console.log(`\n[${new Date().toISOString()}] Shutting down gracefully...`);
779
+ saveRoomState();
780
+
781
+ // Notify all clients
782
+ wss.clients.forEach(ws => {
783
+ if (ws.readyState === WebSocket.OPEN) {
784
+ ws.send(JSON.stringify({
785
+ type: 'server-shutdown',
786
+ message: 'Server is shutting down',
787
+ timestamp: new Date().toISOString()
788
+ }));
789
+ }
790
+ });
791
+
792
+ wss.close(() => {
793
+ console.log(`[${new Date().toISOString()}] Server closed`);
794
+ process.exit(0);
795
+ });
796
+ });
797
+ }
798
+
799
+ start();
800
+
801
+ module.exports = { rooms, clients, Room, User, Client };