cyclecad 3.2.1 → 3.4.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 (65) hide show
  1. package/DOCKER-SETUP-VERIFICATION.md +399 -0
  2. package/DOCKER-TESTING.md +463 -0
  3. package/FUSION360_MODULES.md +478 -0
  4. package/FUSION_MODULES_README.md +352 -0
  5. package/INTEGRATION_SNIPPETS.md +608 -0
  6. package/KILLER-FEATURES-DELIVERY.md +469 -0
  7. package/MODULES_SUMMARY.txt +337 -0
  8. package/QUICK_REFERENCE.txt +298 -0
  9. package/README-DOCKER-TESTING.txt +438 -0
  10. package/app/index.html +23 -10
  11. package/app/js/fusion-help.json +1808 -0
  12. package/app/js/help-module-v3.js +1096 -0
  13. package/app/js/killer-features-help.json +395 -0
  14. package/app/js/killer-features.js +1508 -0
  15. package/app/js/modules/fusion-assembly.js +842 -0
  16. package/app/js/modules/fusion-cam.js +785 -0
  17. package/app/js/modules/fusion-data.js +814 -0
  18. package/app/js/modules/fusion-drawing.js +844 -0
  19. package/app/js/modules/fusion-inspection.js +756 -0
  20. package/app/js/modules/fusion-render.js +774 -0
  21. package/app/js/modules/fusion-simulation.js +986 -0
  22. package/app/js/modules/fusion-sketch.js +1044 -0
  23. package/app/js/modules/fusion-solid.js +1095 -0
  24. package/app/js/modules/fusion-surface.js +949 -0
  25. package/app/tests/FUSION_TEST_SUITE.md +266 -0
  26. package/app/tests/README.md +77 -0
  27. package/app/tests/TESTING-CHECKLIST.md +177 -0
  28. package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
  29. package/app/tests/brep-live-test.html +848 -0
  30. package/app/tests/docker-integration-test.html +811 -0
  31. package/app/tests/fusion-all-tests.html +670 -0
  32. package/app/tests/fusion-assembly-tests.html +461 -0
  33. package/app/tests/fusion-cam-tests.html +421 -0
  34. package/app/tests/fusion-simulation-tests.html +421 -0
  35. package/app/tests/fusion-sketch-tests.html +613 -0
  36. package/app/tests/fusion-solid-tests.html +529 -0
  37. package/app/tests/index.html +453 -0
  38. package/app/tests/killer-features-test.html +509 -0
  39. package/app/tests/run-tests.html +874 -0
  40. package/app/tests/step-import-live-test.html +1115 -0
  41. package/app/tests/test-agent-v3.html +93 -696
  42. package/architecture-dashboard.html +1970 -0
  43. package/docs/API-REFERENCE.md +1423 -0
  44. package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
  45. package/docs/DEVELOPER-GUIDE-v3.md +795 -0
  46. package/docs/DOCKER-QUICK-TEST.md +376 -0
  47. package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
  48. package/docs/FUSION-TUTORIAL.md +1203 -0
  49. package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
  50. package/docs/KEYBOARD-SHORTCUTS.md +402 -0
  51. package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
  52. package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
  53. package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
  54. package/docs/KILLER-FEATURES.md +562 -0
  55. package/docs/QUICK-REFERENCE.md +282 -0
  56. package/docs/README-v3-DOCS.md +274 -0
  57. package/docs/TUTORIAL-v3.md +1190 -0
  58. package/docs/architecture-dashboard.html +1970 -0
  59. package/docs/architecture-v3.html +1038 -0
  60. package/linkedin-post-v3.md +58 -0
  61. package/package.json +1 -1
  62. package/scripts/dev-setup.sh +338 -0
  63. package/scripts/docker-health-check.sh +159 -0
  64. package/scripts/integration-test.sh +311 -0
  65. package/scripts/test-docker.sh +515 -0
@@ -0,0 +1,811 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>cycleCAD Docker Integration Tests</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ }
25
+
26
+ header {
27
+ background: white;
28
+ padding: 30px;
29
+ border-radius: 8px;
30
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
31
+ margin-bottom: 20px;
32
+ }
33
+
34
+ h1 {
35
+ color: #333;
36
+ margin-bottom: 10px;
37
+ font-size: 28px;
38
+ }
39
+
40
+ .header-controls {
41
+ display: flex;
42
+ gap: 10px;
43
+ margin-top: 20px;
44
+ flex-wrap: wrap;
45
+ }
46
+
47
+ button {
48
+ padding: 10px 20px;
49
+ border: none;
50
+ border-radius: 4px;
51
+ font-size: 14px;
52
+ font-weight: 600;
53
+ cursor: pointer;
54
+ transition: all 0.3s ease;
55
+ }
56
+
57
+ .btn-primary {
58
+ background: #667eea;
59
+ color: white;
60
+ }
61
+
62
+ .btn-primary:hover {
63
+ background: #5568d3;
64
+ transform: translateY(-2px);
65
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
66
+ }
67
+
68
+ .btn-primary:disabled {
69
+ background: #ccc;
70
+ cursor: not-allowed;
71
+ transform: none;
72
+ }
73
+
74
+ .btn-secondary {
75
+ background: #e0e0e0;
76
+ color: #333;
77
+ }
78
+
79
+ .btn-secondary:hover {
80
+ background: #d0d0d0;
81
+ }
82
+
83
+ .main {
84
+ display: grid;
85
+ grid-template-columns: 1fr 1fr;
86
+ gap: 20px;
87
+ margin-bottom: 20px;
88
+ }
89
+
90
+ .card {
91
+ background: white;
92
+ padding: 20px;
93
+ border-radius: 8px;
94
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
95
+ }
96
+
97
+ .card h2 {
98
+ font-size: 18px;
99
+ color: #333;
100
+ margin-bottom: 20px;
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 10px;
104
+ }
105
+
106
+ .service-item {
107
+ padding: 15px;
108
+ border-radius: 4px;
109
+ margin-bottom: 10px;
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: space-between;
113
+ border-left: 4px solid #e0e0e0;
114
+ }
115
+
116
+ .service-item.loading {
117
+ background: #f5f5f5;
118
+ border-left-color: #ffa500;
119
+ }
120
+
121
+ .service-item.success {
122
+ background: #f0f8f4;
123
+ border-left-color: #4caf50;
124
+ }
125
+
126
+ .service-item.error {
127
+ background: #fef5f5;
128
+ border-left-color: #f44336;
129
+ }
130
+
131
+ .service-name {
132
+ font-weight: 600;
133
+ color: #333;
134
+ }
135
+
136
+ .service-status {
137
+ display: flex;
138
+ align-items: center;
139
+ gap: 8px;
140
+ }
141
+
142
+ .status-badge {
143
+ padding: 4px 12px;
144
+ border-radius: 20px;
145
+ font-size: 12px;
146
+ font-weight: 600;
147
+ text-transform: uppercase;
148
+ }
149
+
150
+ .status-badge.pending {
151
+ background: #fff3cd;
152
+ color: #856404;
153
+ }
154
+
155
+ .status-badge.success {
156
+ background: #d4edda;
157
+ color: #155724;
158
+ }
159
+
160
+ .status-badge.error {
161
+ background: #f8d7da;
162
+ color: #721c24;
163
+ }
164
+
165
+ .spinner {
166
+ width: 16px;
167
+ height: 16px;
168
+ border: 2px solid #f3f3f3;
169
+ border-top: 2px solid #667eea;
170
+ border-radius: 50%;
171
+ animation: spin 1s linear infinite;
172
+ }
173
+
174
+ @keyframes spin {
175
+ 0% { transform: rotate(0deg); }
176
+ 100% { transform: rotate(360deg); }
177
+ }
178
+
179
+ .icon {
180
+ width: 20px;
181
+ height: 20px;
182
+ display: flex;
183
+ align-items: center;
184
+ justify-content: center;
185
+ font-weight: bold;
186
+ }
187
+
188
+ .icon.success {
189
+ color: #4caf50;
190
+ }
191
+
192
+ .icon.error {
193
+ color: #f44336;
194
+ }
195
+
196
+ .test-results {
197
+ margin-top: 20px;
198
+ }
199
+
200
+ .test-item {
201
+ padding: 12px;
202
+ margin-bottom: 8px;
203
+ border-radius: 4px;
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 10px;
207
+ font-size: 14px;
208
+ }
209
+
210
+ .test-item.pass {
211
+ background: #f0f8f4;
212
+ color: #155724;
213
+ }
214
+
215
+ .test-item.fail {
216
+ background: #fef5f5;
217
+ color: #721c24;
218
+ }
219
+
220
+ .test-item.pending {
221
+ background: #f5f5f5;
222
+ color: #666;
223
+ }
224
+
225
+ .test-icon {
226
+ width: 20px;
227
+ height: 20px;
228
+ display: flex;
229
+ align-items: center;
230
+ justify-content: center;
231
+ font-weight: bold;
232
+ flex-shrink: 0;
233
+ }
234
+
235
+ .test-name {
236
+ flex: 1;
237
+ }
238
+
239
+ .test-time {
240
+ font-size: 12px;
241
+ opacity: 0.7;
242
+ }
243
+
244
+ .stats {
245
+ display: grid;
246
+ grid-template-columns: repeat(4, 1fr);
247
+ gap: 10px;
248
+ margin-bottom: 20px;
249
+ }
250
+
251
+ .stat-card {
252
+ background: white;
253
+ padding: 15px;
254
+ border-radius: 4px;
255
+ text-align: center;
256
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
257
+ }
258
+
259
+ .stat-value {
260
+ font-size: 28px;
261
+ font-weight: bold;
262
+ color: #667eea;
263
+ }
264
+
265
+ .stat-label {
266
+ font-size: 12px;
267
+ color: #999;
268
+ margin-top: 5px;
269
+ text-transform: uppercase;
270
+ }
271
+
272
+ .stat-card.error .stat-value {
273
+ color: #f44336;
274
+ }
275
+
276
+ .export-section {
277
+ margin-top: 20px;
278
+ padding: 15px;
279
+ background: #f5f5f5;
280
+ border-radius: 4px;
281
+ }
282
+
283
+ .export-section h3 {
284
+ font-size: 14px;
285
+ margin-bottom: 10px;
286
+ color: #333;
287
+ }
288
+
289
+ .export-buttons {
290
+ display: flex;
291
+ gap: 10px;
292
+ }
293
+
294
+ .export-buttons button {
295
+ flex: 1;
296
+ padding: 8px;
297
+ font-size: 12px;
298
+ }
299
+
300
+ @media (max-width: 768px) {
301
+ .main {
302
+ grid-template-columns: 1fr;
303
+ }
304
+
305
+ .stats {
306
+ grid-template-columns: repeat(2, 1fr);
307
+ }
308
+
309
+ .header-controls {
310
+ flex-direction: column;
311
+ }
312
+
313
+ button {
314
+ width: 100%;
315
+ }
316
+ }
317
+ </style>
318
+ </head>
319
+ <body>
320
+ <div class="container">
321
+ <header>
322
+ <h1>cycleCAD Docker Integration Tests</h1>
323
+ <p>Real-time monitoring of Docker services and endpoints</p>
324
+ <div class="header-controls">
325
+ <button class="btn-primary" id="runAllTests">Run All Tests</button>
326
+ <button class="btn-primary" id="quickCheck">Quick Health Check</button>
327
+ <button class="btn-secondary" id="clearResults">Clear Results</button>
328
+ </div>
329
+ </header>
330
+
331
+ <div class="stats">
332
+ <div class="stat-card">
333
+ <div class="stat-value" id="totalCount">0</div>
334
+ <div class="stat-label">Total Tests</div>
335
+ </div>
336
+ <div class="stat-card">
337
+ <div class="stat-value" id="passCount" style="color: #4caf50;">0</div>
338
+ <div class="stat-label">Passed</div>
339
+ </div>
340
+ <div class="stat-card">
341
+ <div class="stat-value" id="failCount" style="color: #f44336;">0</div>
342
+ <div class="stat-label">Failed</div>
343
+ </div>
344
+ <div class="stat-card">
345
+ <div class="stat-value" id="successRate">0%</div>
346
+ <div class="stat-label">Success Rate</div>
347
+ </div>
348
+ </div>
349
+
350
+ <div class="main">
351
+ <!-- Service Status -->
352
+ <div class="card">
353
+ <h2>
354
+ <span>Service Status</span>
355
+ <span id="overallStatus" class="status-badge pending">Checking...</span>
356
+ </h2>
357
+ <div id="serviceStatus"></div>
358
+ </div>
359
+
360
+ <!-- CORS & Headers -->
361
+ <div class="card">
362
+ <h2>Headers & Security</h2>
363
+ <div id="headersStatus"></div>
364
+ </div>
365
+
366
+ <!-- Endpoint Tests -->
367
+ <div class="card">
368
+ <h2>Endpoint Tests</h2>
369
+ <div id="endpointTests" class="test-results"></div>
370
+ </div>
371
+
372
+ <!-- Proxy Tests -->
373
+ <div class="card">
374
+ <h2>Proxy Routing</h2>
375
+ <div id="proxyTests" class="test-results"></div>
376
+ </div>
377
+
378
+ <!-- WebSocket Tests -->
379
+ <div class="card">
380
+ <h2>WebSocket & Real-time</h2>
381
+ <div id="websocketTests" class="test-results"></div>
382
+ </div>
383
+
384
+ <!-- STEP Converter -->
385
+ <div class="card">
386
+ <h2>STEP Converter</h2>
387
+ <div id="converterTests" class="test-results"></div>
388
+ </div>
389
+ </div>
390
+
391
+ <!-- Export Section -->
392
+ <div class="card">
393
+ <div class="export-section">
394
+ <h3>Export Results</h3>
395
+ <div class="export-buttons">
396
+ <button class="btn-secondary" id="exportJSON">Export as JSON</button>
397
+ <button class="btn-secondary" id="exportHTML">Export as HTML</button>
398
+ <button class="btn-secondary" id="copyResults">Copy to Clipboard</button>
399
+ </div>
400
+ </div>
401
+ </div>
402
+ </div>
403
+
404
+ <script>
405
+ // Test configuration
406
+ const API_BASE = window.location.origin;
407
+ const SERVICES = {
408
+ main: { name: 'Main App', url: `${API_BASE}/health`, expectedStatus: 200 },
409
+ converter: { name: 'STEP Converter', url: `${API_BASE}:8787/health`, expectedStatus: 200 },
410
+ signaling: { name: 'Signaling Server', url: `${API_BASE}:8788/health`, expectedStatus: 200 },
411
+ };
412
+
413
+ // Test state
414
+ let testResults = {
415
+ services: {},
416
+ headers: {},
417
+ endpoints: {},
418
+ proxies: {},
419
+ websocket: {},
420
+ converter: {},
421
+ startTime: null,
422
+ endTime: null,
423
+ };
424
+
425
+ // Helper functions
426
+ const ui = {
427
+ updateServiceStatus(serviceName, status) {
428
+ const container = document.getElementById('serviceStatus');
429
+ let item = document.getElementById(`service-${serviceName}`);
430
+
431
+ if (!item) {
432
+ item = document.createElement('div');
433
+ item.id = `service-${serviceName}`;
434
+ item.className = 'service-item';
435
+ container.appendChild(item);
436
+ }
437
+
438
+ const statusClass = status === 'success' ? 'success' : status === 'error' ? 'error' : 'loading';
439
+ item.className = `service-item ${statusClass}`;
440
+
441
+ const service = SERVICES[serviceName];
442
+ const icon = status === 'success' ? '✓' : status === 'error' ? '✗' : '●';
443
+ const badge = status === 'success' ? 'success' : status === 'error' ? 'error' : 'pending';
444
+
445
+ item.innerHTML = `
446
+ <span class="service-name">${service.name}</span>
447
+ <div class="service-status">
448
+ <span class="status-badge ${badge}">${status}</span>
449
+ <div class="icon">${icon}</div>
450
+ </div>
451
+ `;
452
+ },
453
+
454
+ addTest(category, name, status, duration) {
455
+ const container = document.getElementById(`${category}Tests`) || document.getElementById('endpointTests');
456
+ const testItem = document.createElement('div');
457
+ testItem.className = `test-item ${status}`;
458
+
459
+ const icon = status === 'pass' ? '✓' : status === 'fail' ? '✗' : '●';
460
+ const iconClass = status === 'pass' ? 'success' : status === 'fail' ? 'error' : '';
461
+
462
+ testItem.innerHTML = `
463
+ <div class="test-icon" style="color: ${iconClass === 'success' ? '#4caf50' : iconClass === 'error' ? '#f44336' : '#999'}">${icon}</div>
464
+ <div class="test-name">${name}</div>
465
+ <div class="test-time">${duration}ms</div>
466
+ `;
467
+
468
+ container.appendChild(testItem);
469
+ },
470
+
471
+ updateStats() {
472
+ const total = Object.values(testResults).reduce((sum, cat) => {
473
+ if (typeof cat === 'object' && !Array.isArray(cat) && cat !== null) {
474
+ return sum + Object.keys(cat).length;
475
+ }
476
+ return sum;
477
+ }, 0);
478
+
479
+ const passed = Object.values(testResults).reduce((sum, cat) => {
480
+ if (typeof cat === 'object' && !Array.isArray(cat) && cat !== null) {
481
+ return sum + Object.values(cat).filter(r => r.status === 'pass').length;
482
+ }
483
+ return sum;
484
+ }, 0);
485
+
486
+ const failed = Object.values(testResults).reduce((sum, cat) => {
487
+ if (typeof cat === 'object' && !Array.isArray(cat) && cat !== null) {
488
+ return sum + Object.values(cat).filter(r => r.status === 'fail').length;
489
+ }
490
+ return sum;
491
+ }, 0);
492
+
493
+ document.getElementById('totalCount').textContent = total;
494
+ document.getElementById('passCount').textContent = passed;
495
+ document.getElementById('failCount').textContent = failed;
496
+ document.getElementById('successRate').textContent = total > 0 ? `${Math.round(passed / total * 100)}%` : '0%';
497
+
498
+ const overallStatus = failed === 0 && total > 0 ? 'success' : failed > 0 ? 'error' : 'pending';
499
+ const statusEl = document.getElementById('overallStatus');
500
+ statusEl.textContent = overallStatus.toUpperCase();
501
+ statusEl.className = `status-badge ${overallStatus}`;
502
+ },
503
+
504
+ clearResults() {
505
+ document.getElementById('serviceStatus').innerHTML = '';
506
+ document.getElementById('headersStatus').innerHTML = '';
507
+ document.getElementById('endpointTests').innerHTML = '';
508
+ document.getElementById('proxyTests').innerHTML = '';
509
+ document.getElementById('websocketTests').innerHTML = '';
510
+ document.getElementById('converterTests').innerHTML = '';
511
+ testResults = {
512
+ services: {},
513
+ headers: {},
514
+ endpoints: {},
515
+ proxies: {},
516
+ websocket: {},
517
+ converter: {},
518
+ startTime: null,
519
+ endTime: null,
520
+ };
521
+ },
522
+ };
523
+
524
+ // Test functions
525
+ async function testService(serviceName) {
526
+ ui.updateServiceStatus(serviceName, 'loading');
527
+ const service = SERVICES[serviceName];
528
+
529
+ try {
530
+ const start = performance.now();
531
+ const response = await fetch(service.url);
532
+ const duration = Math.round(performance.now() - start);
533
+
534
+ if (response.ok) {
535
+ const data = await response.json();
536
+ testResults.services[serviceName] = { status: 'pass', duration, data };
537
+ ui.updateServiceStatus(serviceName, 'success');
538
+ return { status: 'pass', duration };
539
+ } else {
540
+ testResults.services[serviceName] = { status: 'fail', duration, error: 'Status ' + response.status };
541
+ ui.updateServiceStatus(serviceName, 'error');
542
+ return { status: 'fail', duration };
543
+ }
544
+ } catch (err) {
545
+ testResults.services[serviceName] = { status: 'fail', duration: 0, error: err.message };
546
+ ui.updateServiceStatus(serviceName, 'error');
547
+ return { status: 'fail', duration: 0 };
548
+ }
549
+ }
550
+
551
+ async function testCORSHeaders() {
552
+ const start = performance.now();
553
+
554
+ try {
555
+ const response = await fetch(`${API_BASE}/health`, {
556
+ headers: { 'Origin': 'http://example.com' }
557
+ });
558
+ const duration = Math.round(performance.now() - start);
559
+
560
+ const hasCORS = response.headers.has('access-control-allow-origin');
561
+ const hasCOOP = response.headers.has('cross-origin-opener-policy');
562
+ const hasCoEP = response.headers.has('cross-origin-embedder-policy');
563
+
564
+ const status = hasCORS && hasCOOP && hasCoEP ? 'pass' : 'fail';
565
+ testResults.headers['cors'] = { status, duration, headers: {
566
+ cors: hasCORS,
567
+ coop: hasCOOP,
568
+ coep: hasCoEP,
569
+ }};
570
+
571
+ ui.addTest('headers', 'CORS Headers Present', status, duration);
572
+ return { status, duration };
573
+ } catch (err) {
574
+ testResults.headers['cors'] = { status: 'fail', duration: 0, error: err.message };
575
+ ui.addTest('headers', 'CORS Headers Present', 'fail', 0);
576
+ return { status: 'fail', duration: 0 };
577
+ }
578
+ }
579
+
580
+ async function testGzipCompression() {
581
+ const start = performance.now();
582
+
583
+ try {
584
+ const response = await fetch(`${API_BASE}/app/`, {
585
+ headers: { 'Accept-Encoding': 'gzip' }
586
+ });
587
+ const duration = Math.round(performance.now() - start);
588
+
589
+ const hasGzip = response.headers.get('content-encoding') === 'gzip';
590
+ testResults.headers['gzip'] = { status: hasGzip ? 'pass' : 'fail', duration };
591
+
592
+ ui.addTest('headers', 'Gzip Compression', hasGzip ? 'pass' : 'fail', duration);
593
+ return { status: hasGzip ? 'pass' : 'fail', duration };
594
+ } catch (err) {
595
+ testResults.headers['gzip'] = { status: 'fail', duration: 0, error: err.message };
596
+ ui.addTest('headers', 'Gzip Compression', 'fail', 0);
597
+ return { status: 'fail', duration: 0 };
598
+ }
599
+ }
600
+
601
+ async function testEndpoint(name, url, expectedStatus = 200) {
602
+ const start = performance.now();
603
+
604
+ try {
605
+ const response = await fetch(url);
606
+ const duration = Math.round(performance.now() - start);
607
+ const status = response.status === expectedStatus ? 'pass' : 'fail';
608
+ testResults.endpoints[name] = { status, duration, httpStatus: response.status };
609
+ ui.addTest('endpoint', name, status, duration);
610
+ return { status, duration };
611
+ } catch (err) {
612
+ testResults.endpoints[name] = { status: 'fail', duration: 0, error: err.message };
613
+ ui.addTest('endpoint', name, 'fail', 0);
614
+ return { status: 'fail', duration: 0 };
615
+ }
616
+ }
617
+
618
+ async function testProxyRoute(name, url) {
619
+ const start = performance.now();
620
+
621
+ try {
622
+ const response = await fetch(url);
623
+ const duration = Math.round(performance.now() - start);
624
+ const status = response.ok ? 'pass' : 'fail';
625
+ testResults.proxies[name] = { status, duration, httpStatus: response.status };
626
+ ui.addTest('proxy', name, status, duration);
627
+ return { status, duration };
628
+ } catch (err) {
629
+ testResults.proxies[name] = { status: 'fail', duration: 0, error: err.message };
630
+ ui.addTest('proxy', name, 'fail', 0);
631
+ return { status: 'fail', duration: 0 };
632
+ }
633
+ }
634
+
635
+ async function testWebSocket() {
636
+ const start = performance.now();
637
+
638
+ try {
639
+ const ws = new WebSocket(`ws://${window.location.host}/ws/`);
640
+
641
+ return new Promise((resolve) => {
642
+ const timeout = setTimeout(() => {
643
+ ws.close();
644
+ const duration = Math.round(performance.now() - start);
645
+ testResults.websocket['connection'] = { status: 'fail', duration, error: 'Timeout' };
646
+ ui.addTest('websocket', 'WebSocket Connection', 'fail', duration);
647
+ resolve({ status: 'fail', duration });
648
+ }, 5000);
649
+
650
+ ws.onopen = () => {
651
+ clearTimeout(timeout);
652
+ ws.close();
653
+ const duration = Math.round(performance.now() - start);
654
+ testResults.websocket['connection'] = { status: 'pass', duration };
655
+ ui.addTest('websocket', 'WebSocket Connection', 'pass', duration);
656
+ resolve({ status: 'pass', duration });
657
+ };
658
+
659
+ ws.onerror = () => {
660
+ clearTimeout(timeout);
661
+ const duration = Math.round(performance.now() - start);
662
+ testResults.websocket['connection'] = { status: 'fail', duration, error: 'Connection error' };
663
+ ui.addTest('websocket', 'WebSocket Connection', 'fail', duration);
664
+ resolve({ status: 'fail', duration });
665
+ };
666
+ });
667
+ } catch (err) {
668
+ testResults.websocket['connection'] = { status: 'fail', duration: 0, error: err.message };
669
+ ui.addTest('websocket', 'WebSocket Connection', 'fail', 0);
670
+ return { status: 'fail', duration: 0 };
671
+ }
672
+ }
673
+
674
+ async function testConverterEndpoint() {
675
+ const start = performance.now();
676
+
677
+ try {
678
+ const response = await fetch(`${API_BASE}:8787/health`);
679
+ const duration = Math.round(performance.now() - start);
680
+
681
+ if (response.ok) {
682
+ const data = await response.json();
683
+ testResults.converter['health'] = { status: 'pass', duration, data };
684
+ ui.addTest('converter', 'STEP Converter /health', 'pass', duration);
685
+ return { status: 'pass', duration };
686
+ } else {
687
+ testResults.converter['health'] = { status: 'fail', duration, error: 'Status ' + response.status };
688
+ ui.addTest('converter', 'STEP Converter /health', 'fail', duration);
689
+ return { status: 'fail', duration };
690
+ }
691
+ } catch (err) {
692
+ testResults.converter['health'] = { status: 'fail', duration: 0, error: err.message };
693
+ ui.addTest('converter', 'STEP Converter /health', 'fail', 0);
694
+ return { status: 'fail', duration: 0 };
695
+ }
696
+ }
697
+
698
+ // Main test runner
699
+ async function runAllTests() {
700
+ testResults.startTime = new Date();
701
+ ui.clearResults();
702
+
703
+ // Run service tests
704
+ await testService('main');
705
+ await testService('converter');
706
+ await testService('signaling');
707
+
708
+ // Run header tests
709
+ await testCORSHeaders();
710
+ await testGzipCompression();
711
+
712
+ // Run endpoint tests
713
+ await testEndpoint('GET /app/', `${API_BASE}/app/`, 200);
714
+ await testEndpoint('GET /', `${API_BASE}/`, 200);
715
+ await testEndpoint('GET /health', `${API_BASE}/health`, 200);
716
+
717
+ // Run proxy tests
718
+ await testProxyRoute('Converter via /converter/', `${API_BASE}/converter/health`);
719
+ await testProxyRoute('API via /api/', `${API_BASE}/api/health`);
720
+
721
+ // Run WebSocket test
722
+ await testWebSocket();
723
+
724
+ // Run converter tests
725
+ await testConverterEndpoint();
726
+
727
+ testResults.endTime = new Date();
728
+ ui.updateStats();
729
+ }
730
+
731
+ async function quickHealthCheck() {
732
+ testResults.startTime = new Date();
733
+ ui.clearResults();
734
+
735
+ await testService('main');
736
+ await testService('converter');
737
+ await testService('signaling');
738
+
739
+ testResults.endTime = new Date();
740
+ ui.updateStats();
741
+ }
742
+
743
+ // Export functions
744
+ function exportAsJSON() {
745
+ const json = JSON.stringify(testResults, null, 2);
746
+ const blob = new Blob([json], { type: 'application/json' });
747
+ downloadFile(blob, 'docker-tests-results.json');
748
+ }
749
+
750
+ function exportAsHTML() {
751
+ const html = `<!DOCTYPE html>
752
+ <html>
753
+ <head>
754
+ <title>cycleCAD Docker Test Results</title>
755
+ <style>
756
+ body { font-family: Arial; margin: 20px; }
757
+ h1 { color: #667eea; }
758
+ .pass { background: #d4edda; padding: 10px; margin: 5px 0; border-radius: 4px; }
759
+ .fail { background: #f8d7da; padding: 10px; margin: 5px 0; border-radius: 4px; }
760
+ </style>
761
+ </head>
762
+ <body>
763
+ <h1>cycleCAD Docker Integration Test Results</h1>
764
+ <p>Generated: ${new Date().toISOString()}</p>
765
+ <h2>Summary</h2>
766
+ <p>Total: ${document.getElementById('totalCount').textContent}</p>
767
+ <p>Passed: ${document.getElementById('passCount').textContent}</p>
768
+ <p>Failed: ${document.getElementById('failCount').textContent}</p>
769
+ <p>Success Rate: ${document.getElementById('successRate').textContent}</p>
770
+ <h2>Detailed Results</h2>
771
+ <pre>${JSON.stringify(testResults, null, 2)}</pre>
772
+ </body>
773
+ </html>`;
774
+ const blob = new Blob([html], { type: 'text/html' });
775
+ downloadFile(blob, 'docker-tests-results.html');
776
+ }
777
+
778
+ function downloadFile(blob, filename) {
779
+ const url = URL.createObjectURL(blob);
780
+ const a = document.createElement('a');
781
+ a.href = url;
782
+ a.download = filename;
783
+ document.body.appendChild(a);
784
+ a.click();
785
+ document.body.removeChild(a);
786
+ URL.revokeObjectURL(url);
787
+ }
788
+
789
+ function copyToClipboard() {
790
+ const json = JSON.stringify(testResults, null, 2);
791
+ navigator.clipboard.writeText(json).then(() => {
792
+ alert('Results copied to clipboard!');
793
+ });
794
+ }
795
+
796
+ // Event listeners
797
+ document.getElementById('runAllTests').addEventListener('click', runAllTests);
798
+ document.getElementById('quickCheck').addEventListener('click', quickHealthCheck);
799
+ document.getElementById('clearResults').addEventListener('click', () => {
800
+ ui.clearResults();
801
+ ui.updateStats();
802
+ });
803
+ document.getElementById('exportJSON').addEventListener('click', exportAsJSON);
804
+ document.getElementById('exportHTML').addEventListener('click', exportAsHTML);
805
+ document.getElementById('copyResults').addEventListener('click', copyToClipboard);
806
+
807
+ // Auto-run on load
808
+ window.addEventListener('load', quickHealthCheck);
809
+ </script>
810
+ </body>
811
+ </html>