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,1134 @@
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 PWA Test Suite</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, sans-serif;
16
+ background: #1e1e1e;
17
+ color: #fff;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ }
25
+
26
+ header {
27
+ margin-bottom: 30px;
28
+ }
29
+
30
+ h1 {
31
+ font-size: 28px;
32
+ margin-bottom: 8px;
33
+ }
34
+
35
+ .subtitle {
36
+ color: #9ca3af;
37
+ font-size: 14px;
38
+ }
39
+
40
+ .toolbar {
41
+ display: flex;
42
+ gap: 10px;
43
+ margin-bottom: 20px;
44
+ flex-wrap: wrap;
45
+ }
46
+
47
+ button {
48
+ padding: 10px 20px;
49
+ border-radius: 6px;
50
+ border: none;
51
+ font-weight: 600;
52
+ cursor: pointer;
53
+ transition: all 0.2s ease;
54
+ font-size: 14px;
55
+ }
56
+
57
+ .btn-primary {
58
+ background: #0284C7;
59
+ color: white;
60
+ }
61
+
62
+ .btn-primary:hover {
63
+ background: #0369a1;
64
+ }
65
+
66
+ .btn-secondary {
67
+ background: #374151;
68
+ color: white;
69
+ }
70
+
71
+ .btn-secondary:hover {
72
+ background: #4b5563;
73
+ }
74
+
75
+ .btn-success {
76
+ background: #10b981;
77
+ color: white;
78
+ }
79
+
80
+ .btn-success:hover {
81
+ background: #059669;
82
+ }
83
+
84
+ .btn-danger {
85
+ background: #ef4444;
86
+ color: white;
87
+ }
88
+
89
+ .btn-danger:hover {
90
+ background: #dc2626;
91
+ }
92
+
93
+ .test-grid {
94
+ display: grid;
95
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
96
+ gap: 20px;
97
+ margin-bottom: 30px;
98
+ }
99
+
100
+ .test-card {
101
+ background: #2d2d2d;
102
+ border: 1px solid #374151;
103
+ border-radius: 8px;
104
+ padding: 16px;
105
+ transition: all 0.2s ease;
106
+ }
107
+
108
+ .test-card:hover {
109
+ border-color: #0284C7;
110
+ box-shadow: 0 0 10px rgba(2, 132, 199, 0.2);
111
+ }
112
+
113
+ .test-name {
114
+ font-weight: 600;
115
+ margin-bottom: 8px;
116
+ font-size: 14px;
117
+ }
118
+
119
+ .test-desc {
120
+ font-size: 13px;
121
+ color: #9ca3af;
122
+ margin-bottom: 12px;
123
+ line-height: 1.4;
124
+ }
125
+
126
+ .test-status {
127
+ display: inline-block;
128
+ padding: 4px 12px;
129
+ border-radius: 4px;
130
+ font-size: 12px;
131
+ font-weight: 600;
132
+ margin-bottom: 12px;
133
+ }
134
+
135
+ .status-pending {
136
+ background: rgba(107, 114, 128, 0.2);
137
+ color: #d1d5db;
138
+ }
139
+
140
+ .status-running {
141
+ background: rgba(245, 158, 11, 0.2);
142
+ color: #fbbf24;
143
+ }
144
+
145
+ .status-pass {
146
+ background: rgba(16, 185, 129, 0.2);
147
+ color: #86efac;
148
+ }
149
+
150
+ .status-fail {
151
+ background: rgba(239, 68, 68, 0.2);
152
+ color: #fca5a5;
153
+ }
154
+
155
+ .test-actions {
156
+ display: flex;
157
+ gap: 8px;
158
+ }
159
+
160
+ .test-actions button {
161
+ padding: 6px 12px;
162
+ font-size: 12px;
163
+ flex: 1;
164
+ }
165
+
166
+ .results-panel {
167
+ background: #2d2d2d;
168
+ border: 1px solid #374151;
169
+ border-radius: 8px;
170
+ padding: 20px;
171
+ margin-bottom: 20px;
172
+ max-height: 400px;
173
+ overflow-y: auto;
174
+ }
175
+
176
+ .results-header {
177
+ font-weight: 600;
178
+ margin-bottom: 12px;
179
+ font-size: 14px;
180
+ color: #0284C7;
181
+ }
182
+
183
+ .result-item {
184
+ padding: 10px;
185
+ margin-bottom: 8px;
186
+ border-radius: 4px;
187
+ font-size: 13px;
188
+ font-family: 'Monaco', 'Courier New', monospace;
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 8px;
192
+ }
193
+
194
+ .result-item.pass {
195
+ background: rgba(16, 185, 129, 0.1);
196
+ color: #86efac;
197
+ }
198
+
199
+ .result-item.fail {
200
+ background: rgba(239, 68, 68, 0.1);
201
+ color: #fca5a5;
202
+ }
203
+
204
+ .result-item.info {
205
+ background: rgba(59, 130, 246, 0.1);
206
+ color: #93c5fd;
207
+ }
208
+
209
+ .result-icon {
210
+ font-weight: bold;
211
+ }
212
+
213
+ .stats {
214
+ display: grid;
215
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
216
+ gap: 15px;
217
+ margin-bottom: 20px;
218
+ }
219
+
220
+ .stat-card {
221
+ background: #2d2d2d;
222
+ border: 1px solid #374151;
223
+ border-radius: 8px;
224
+ padding: 16px;
225
+ text-align: center;
226
+ }
227
+
228
+ .stat-number {
229
+ font-size: 28px;
230
+ font-weight: 700;
231
+ margin-bottom: 4px;
232
+ }
233
+
234
+ .stat-label {
235
+ font-size: 12px;
236
+ color: #9ca3af;
237
+ text-transform: uppercase;
238
+ letter-spacing: 0.5px;
239
+ }
240
+
241
+ .progress-bar {
242
+ width: 100%;
243
+ height: 4px;
244
+ background: #374151;
245
+ border-radius: 2px;
246
+ margin: 10px 0;
247
+ overflow: hidden;
248
+ }
249
+
250
+ .progress-fill {
251
+ height: 100%;
252
+ background: #0284C7;
253
+ transition: width 0.3s ease;
254
+ }
255
+
256
+ .category {
257
+ margin-bottom: 40px;
258
+ }
259
+
260
+ .category-title {
261
+ font-size: 18px;
262
+ font-weight: 700;
263
+ margin-bottom: 16px;
264
+ padding-bottom: 12px;
265
+ border-bottom: 2px solid #374151;
266
+ color: #0284C7;
267
+ }
268
+
269
+ .elapsed-time {
270
+ font-size: 12px;
271
+ color: #9ca3af;
272
+ margin-top: 8px;
273
+ }
274
+
275
+ @media (max-width: 768px) {
276
+ .test-grid {
277
+ grid-template-columns: 1fr;
278
+ }
279
+
280
+ .toolbar {
281
+ flex-direction: column;
282
+ }
283
+
284
+ button {
285
+ width: 100%;
286
+ }
287
+ }
288
+ </style>
289
+ </head>
290
+ <body>
291
+ <div class="container">
292
+ <header>
293
+ <h1>cycleCAD PWA Test Suite</h1>
294
+ <p class="subtitle">Automated testing for offline mode, caching, sync, and service worker functionality</p>
295
+ </header>
296
+
297
+ <div class="toolbar">
298
+ <button class="btn-primary" onclick="runAllTests()">▶ Run All Tests</button>
299
+ <button class="btn-secondary" onclick="clearResults()">Clear Results</button>
300
+ <button class="btn-danger" onclick="location.reload()">Reload Page</button>
301
+ </div>
302
+
303
+ <div class="stats">
304
+ <div class="stat-card">
305
+ <div class="stat-number" id="total-count">0</div>
306
+ <div class="stat-label">Total Tests</div>
307
+ </div>
308
+ <div class="stat-card">
309
+ <div class="stat-number" id="pass-count">0</div>
310
+ <div class="stat-label">Passed</div>
311
+ </div>
312
+ <div class="stat-card">
313
+ <div class="stat-number" id="fail-count">0</div>
314
+ <div class="stat-label">Failed</div>
315
+ </div>
316
+ <div class="stat-card">
317
+ <div class="stat-number" id="run-count">0</div>
318
+ <div class="stat-label">Running</div>
319
+ </div>
320
+ </div>
321
+
322
+ <div class="progress-bar">
323
+ <div class="progress-fill" id="progress-fill" style="width: 0%"></div>
324
+ </div>
325
+
326
+ <div class="results-panel" id="results-panel">
327
+ <div class="results-header">Test Results</div>
328
+ <p style="color: #9ca3af; font-size: 13px;">No tests run yet. Click 'Run All Tests' to begin.</p>
329
+ </div>
330
+
331
+ <!-- Service Worker Tests -->
332
+ <div class="category">
333
+ <h2 class="category-title">Service Worker</h2>
334
+ <div class="test-grid">
335
+ <div class="test-card">
336
+ <div class="test-name">SW Registration</div>
337
+ <div class="test-desc">Service Worker can be registered and is active</div>
338
+ <div class="test-status status-pending" id="test-1-status">pending</div>
339
+ <div class="test-actions">
340
+ <button class="btn-secondary" onclick="runTest('test-1')">Test</button>
341
+ </div>
342
+ </div>
343
+
344
+ <div class="test-card">
345
+ <div class="test-name">SW Precache</div>
346
+ <div class="test-desc">Precached files are available in cache</div>
347
+ <div class="test-status status-pending" id="test-2-status">pending</div>
348
+ <div class="test-actions">
349
+ <button class="btn-secondary" onclick="runTest('test-2')">Test</button>
350
+ </div>
351
+ </div>
352
+
353
+ <div class="test-card">
354
+ <div class="test-name">SW Message Handling</div>
355
+ <div class="test-desc">SW can receive and respond to messages</div>
356
+ <div class="test-status status-pending" id="test-3-status">pending</div>
357
+ <div class="test-actions">
358
+ <button class="btn-secondary" onclick="runTest('test-3')">Test</button>
359
+ </div>
360
+ </div>
361
+ </div>
362
+ </div>
363
+
364
+ <!-- Offline Mode Tests -->
365
+ <div class="category">
366
+ <h2 class="category-title">Offline Mode</h2>
367
+ <div class="test-grid">
368
+ <div class="test-card">
369
+ <div class="test-name">Offline Detection</div>
370
+ <div class="test-desc">App detects offline status correctly</div>
371
+ <div class="test-status status-pending" id="test-4-status">pending</div>
372
+ <div class="test-actions">
373
+ <button class="btn-secondary" onclick="runTest('test-4')">Test</button>
374
+ </div>
375
+ </div>
376
+
377
+ <div class="test-card">
378
+ <div class="test-name">Offline Banner</div>
379
+ <div class="test-desc">Offline banner appears when disconnected</div>
380
+ <div class="test-status status-pending" id="test-5-status">pending</div>
381
+ <div class="test-actions">
382
+ <button class="btn-secondary" onclick="runTest('test-5')">Test</button>
383
+ </div>
384
+ </div>
385
+
386
+ <div class="test-card">
387
+ <div class="test-name">Offline Page Load</div>
388
+ <div class="test-desc">App loads from cache when offline</div>
389
+ <div class="test-status status-pending" id="test-6-status">pending</div>
390
+ <div class="test-actions">
391
+ <button class="btn-secondary" onclick="runTest('test-6')">Test</button>
392
+ </div>
393
+ </div>
394
+
395
+ <div class="test-card">
396
+ <div class="test-name">Operation Queue</div>
397
+ <div class="test-desc">Operations queue when offline</div>
398
+ <div class="test-status status-pending" id="test-7-status">pending</div>
399
+ <div class="test-actions">
400
+ <button class="btn-secondary" onclick="runTest('test-7')">Test</button>
401
+ </div>
402
+ </div>
403
+ </div>
404
+ </div>
405
+
406
+ <!-- Caching Tests -->
407
+ <div class="category">
408
+ <h2 class="category-title">Caching Strategies</h2>
409
+ <div class="test-grid">
410
+ <div class="test-card">
411
+ <div class="test-name">Cache-First Strategy</div>
412
+ <div class="test-desc">Static assets use cache-first strategy</div>
413
+ <div class="test-status status-pending" id="test-8-status">pending</div>
414
+ <div class="test-actions">
415
+ <button class="btn-secondary" onclick="runTest('test-8')">Test</button>
416
+ </div>
417
+ </div>
418
+
419
+ <div class="test-card">
420
+ <div class="test-name">Network-First Strategy</div>
421
+ <div class="test-desc">API calls use network-first strategy</div>
422
+ <div class="test-status status-pending" id="test-9-status">pending</div>
423
+ <div class="test-actions">
424
+ <button class="btn-secondary" onclick="runTest('test-9')">Test</button>
425
+ </div>
426
+ </div>
427
+
428
+ <div class="test-card">
429
+ <div class="test-name">Model Cache Size</div>
430
+ <div class="test-desc">Model cache respects size limits</div>
431
+ <div class="test-status status-pending" id="test-10-status">pending</div>
432
+ <div class="test-actions">
433
+ <button class="btn-secondary" onclick="runTest('test-10')">Test</button>
434
+ </div>
435
+ </div>
436
+
437
+ <div class="test-card">
438
+ <div class="test-name">Cache Versioning</div>
439
+ <div class="test-desc">Old cache versions are cleaned up</div>
440
+ <div class="test-status status-pending" id="test-11-status">pending</div>
441
+ <div class="test-actions">
442
+ <button class="btn-secondary" onclick="runTest('test-11')">Test</button>
443
+ </div>
444
+ </div>
445
+
446
+ <div class="test-card">
447
+ <div class="test-name">LRU Eviction</div>
448
+ <div class="test-desc">Oldest files evicted when cache full</div>
449
+ <div class="test-status status-pending" id="test-12-status">pending</div>
450
+ <div class="test-actions">
451
+ <button class="btn-secondary" onclick="runTest('test-12')">Test</button>
452
+ </div>
453
+ </div>
454
+
455
+ <div class="test-card">
456
+ <div class="test-name">Cache Persistence</div>
457
+ <div class="test-desc">Cache survives page reload</div>
458
+ <div class="test-status status-pending" id="test-13-status">pending</div>
459
+ <div class="test-actions">
460
+ <button class="btn-secondary" onclick="runTest('test-13')">Test</button>
461
+ </div>
462
+ </div>
463
+ </div>
464
+ </div>
465
+
466
+ <!-- IndexedDB Tests -->
467
+ <div class="category">
468
+ <h2 class="category-title">Storage & Database</h2>
469
+ <div class="test-grid">
470
+ <div class="test-card">
471
+ <div class="test-name">IndexedDB Available</div>
472
+ <div class="test-desc">IndexedDB is available and working</div>
473
+ <div class="test-status status-pending" id="test-14-status">pending</div>
474
+ <div class="test-actions">
475
+ <button class="btn-secondary" onclick="runTest('test-14')">Test</button>
476
+ </div>
477
+ </div>
478
+
479
+ <div class="test-card">
480
+ <div class="test-name">Project Store</div>
481
+ <div class="test-desc">Can store and retrieve projects</div>
482
+ <div class="test-status status-pending" id="test-15-status">pending</div>
483
+ <div class="test-actions">
484
+ <button class="btn-secondary" onclick="runTest('test-15')">Test</button>
485
+ </div>
486
+ </div>
487
+
488
+ <div class="test-card">
489
+ <div class="test-name">Operation Queue Store</div>
490
+ <div class="test-desc">Operation queue store works correctly</div>
491
+ <div class="test-status status-pending" id="test-16-status">pending</div>
492
+ <div class="test-actions">
493
+ <button class="btn-secondary" onclick="runTest('test-16')">Test</button>
494
+ </div>
495
+ </div>
496
+
497
+ <div class="test-card">
498
+ <div class="test-name">Storage Quota</div>
499
+ <div class="test-desc">Can check storage quota</div>
500
+ <div class="test-status status-pending" id="test-17-status">pending</div>
501
+ <div class="test-actions">
502
+ <button class="btn-secondary" onclick="runTest('test-17')">Test</button>
503
+ </div>
504
+ </div>
505
+ </div>
506
+ </div>
507
+
508
+ <!-- Sync Tests -->
509
+ <div class="category">
510
+ <h2 class="category-title">Background Sync</h2>
511
+ <div class="test-grid">
512
+ <div class="test-card">
513
+ <div class="test-name">Queue Operation</div>
514
+ <div class="test-desc">Can queue operations offline</div>
515
+ <div class="test-status status-pending" id="test-18-status">pending</div>
516
+ <div class="test-actions">
517
+ <button class="btn-secondary" onclick="runTest('test-18')">Test</button>
518
+ </div>
519
+ </div>
520
+
521
+ <div class="test-card">
522
+ <div class="test-name">Sync Operations</div>
523
+ <div class="test-desc">Queued operations sync when online</div>
524
+ <div class="test-status status-pending" id="test-19-status">pending</div>
525
+ <div class="test-actions">
526
+ <button class="btn-secondary" onclick="runTest('test-19')">Test</button>
527
+ </div>
528
+ </div>
529
+
530
+ <div class="test-card">
531
+ <div class="test-name">Sync Order</div>
532
+ <div class="test-desc">Operations sync in correct order</div>
533
+ <div class="test-status status-pending" id="test-20-status">pending</div>
534
+ <div class="test-actions">
535
+ <button class="btn-secondary" onclick="runTest('test-20')">Test</button>
536
+ </div>
537
+ </div>
538
+
539
+ <div class="test-card">
540
+ <div class="test-name">Sync Progress</div>
541
+ <div class="test-desc">Sync progress is reported correctly</div>
542
+ <div class="test-status status-pending" id="test-21-status">pending</div>
543
+ <div class="test-actions">
544
+ <button class="btn-secondary" onclick="runTest('test-21')">Test</button>
545
+ </div>
546
+ </div>
547
+ </div>
548
+ </div>
549
+
550
+ <!-- Manifest Tests -->
551
+ <div class="category">
552
+ <h2 class="category-title">Web App Manifest</h2>
553
+ <div class="test-grid">
554
+ <div class="test-card">
555
+ <div class="test-name">Manifest Valid</div>
556
+ <div class="test-desc">manifest.json is valid JSON</div>
557
+ <div class="test-status status-pending" id="test-22-status">pending</div>
558
+ <div class="test-actions">
559
+ <button class="btn-secondary" onclick="runTest('test-22')">Test</button>
560
+ </div>
561
+ </div>
562
+
563
+ <div class="test-card">
564
+ <div class="test-name">Manifest Required Fields</div>
565
+ <div class="test-desc">Required manifest fields present</div>
566
+ <div class="test-status status-pending" id="test-23-status">pending</div>
567
+ <div class="test-actions">
568
+ <button class="btn-secondary" onclick="runTest('test-23')">Test</button>
569
+ </div>
570
+ </div>
571
+
572
+ <div class="test-card">
573
+ <div class="test-name">Start URL</div>
574
+ <div class="test-desc">Start URL is accessible</div>
575
+ <div class="test-status status-pending" id="test-24-status">pending</div>
576
+ <div class="test-actions">
577
+ <button class="btn-secondary" onclick="runTest('test-24')">Test</button>
578
+ </div>
579
+ </div>
580
+
581
+ <div class="test-card">
582
+ <div class="test-name">Icons Accessible</div>
583
+ <div class="test-desc">App icons are accessible</div>
584
+ <div class="test-status status-pending" id="test-25-status">pending</div>
585
+ <div class="test-actions">
586
+ <button class="btn-secondary" onclick="runTest('test-25')">Test</button>
587
+ </div>
588
+ </div>
589
+ </div>
590
+ </div>
591
+
592
+ <!-- Install & Update Tests -->
593
+ <div class="category">
594
+ <h2 class="category-title">Install & Updates</h2>
595
+ <div class="test-grid">
596
+ <div class="test-card">
597
+ <div class="test-name">Install Prompt</div>
598
+ <div class="test-desc">Install prompt can be triggered</div>
599
+ <div class="test-status status-pending" id="test-26-status">pending</div>
600
+ <div class="test-actions">
601
+ <button class="btn-secondary" onclick="runTest('test-26')">Test</button>
602
+ </div>
603
+ </div>
604
+
605
+ <div class="test-card">
606
+ <div class="test-name">Update Detection</div>
607
+ <div class="test-desc">App can detect new versions</div>
608
+ <div class="test-status status-pending" id="test-27-status">pending</div>
609
+ <div class="test-actions">
610
+ <button class="btn-secondary" onclick="runTest('test-27')">Test</button>
611
+ </div>
612
+ </div>
613
+
614
+ <div class="test-card">
615
+ <div class="test-name">HTTPS Requirement</div>
616
+ <div class="test-desc">PWA is served over HTTPS</div>
617
+ <div class="test-status status-pending" id="test-28-status">pending</div>
618
+ <div class="test-actions">
619
+ <button class="btn-secondary" onclick="runTest('test-28')">Test</button>
620
+ </div>
621
+ </div>
622
+ </div>
623
+ </div>
624
+ </div>
625
+
626
+ <script src="/app/js/offline-manager.js"></script>
627
+ <script>
628
+ const tests = [];
629
+ let passCount = 0;
630
+ let failCount = 0;
631
+ let totalCount = 0;
632
+
633
+ class TestRunner {
634
+ constructor(id, name, fn) {
635
+ this.id = id;
636
+ this.name = name;
637
+ this.fn = fn;
638
+ this.status = 'pending';
639
+ this.result = null;
640
+ }
641
+
642
+ async run() {
643
+ this.status = 'running';
644
+ updateStatus(this.id, 'running');
645
+
646
+ try {
647
+ const startTime = performance.now();
648
+ await this.fn();
649
+ const duration = performance.now() - startTime;
650
+
651
+ this.status = 'pass';
652
+ this.result = `Passed in ${duration.toFixed(2)}ms`;
653
+ updateStatus(this.id, 'pass', this.result);
654
+ passCount++;
655
+ return true;
656
+ } catch (err) {
657
+ this.status = 'fail';
658
+ this.result = err.message;
659
+ updateStatus(this.id, 'fail', this.result);
660
+ failCount++;
661
+ return false;
662
+ }
663
+ }
664
+ }
665
+
666
+ // Test: SW Registration
667
+ tests.push(new TestRunner('test-1', 'SW Registration', async () => {
668
+ if (!('serviceWorker' in navigator)) {
669
+ throw new Error('Service Workers not supported');
670
+ }
671
+
672
+ const reg = await navigator.serviceWorker.getRegistration('/app/');
673
+ if (!reg || !reg.active) {
674
+ throw new Error('Service Worker not active. Ensure sw.js is registered in index.html');
675
+ }
676
+
677
+ log('✓', `Service Worker active: ${reg.active.state}`);
678
+ }));
679
+
680
+ // Test: SW Precache
681
+ tests.push(new TestRunner('test-2', 'SW Precache', async () => {
682
+ const cache = await caches.open('cyclecad-static-v3');
683
+ if (!cache) throw new Error('Cache not found');
684
+
685
+ const keys = await cache.keys();
686
+ if (keys.length === 0) {
687
+ throw new Error('No precached files found');
688
+ }
689
+
690
+ log('✓', `${keys.length} files precached`);
691
+ }));
692
+
693
+ // Test: SW Message Handling
694
+ tests.push(new TestRunner('test-3', 'SW Message Handling', async () => {
695
+ return new Promise((resolve, reject) => {
696
+ if (!navigator.serviceWorker.controller) {
697
+ reject(new Error('No active service worker'));
698
+ return;
699
+ }
700
+
701
+ const channel = new MessageChannel();
702
+ navigator.serviceWorker.controller.postMessage(
703
+ { type: 'GET_CACHE_SIZE' },
704
+ [channel.port2]
705
+ );
706
+
707
+ const timeout = setTimeout(() => {
708
+ reject(new Error('Message timeout'));
709
+ }, 5000);
710
+
711
+ channel.port1.onmessage = (event) => {
712
+ clearTimeout(timeout);
713
+ if (event.data.size !== undefined) {
714
+ log('✓', `Cache size: ${formatBytes(event.data.size)}`);
715
+ resolve();
716
+ } else {
717
+ reject(new Error('Invalid response'));
718
+ }
719
+ };
720
+ });
721
+ }));
722
+
723
+ // Test: Offline Detection
724
+ tests.push(new TestRunner('test-4', 'Offline Detection', async () => {
725
+ if (window.offlineManager === undefined) {
726
+ throw new Error('OfflineManager not loaded');
727
+ }
728
+
729
+ const isOnline = window.offlineManager.isOnline;
730
+ log('✓', `Current status: ${isOnline ? 'Online' : 'Offline'}`);
731
+ }));
732
+
733
+ // Test: Offline Banner
734
+ tests.push(new TestRunner('test-5', 'Offline Banner', async () => {
735
+ // This test would require manually going offline
736
+ log('ℹ', 'Manual test: Go offline (F12 > Network) and check for red banner');
737
+ }));
738
+
739
+ // Test: Offline Page Load
740
+ tests.push(new TestRunner('test-6', 'Offline Page Load', async () => {
741
+ const response = await caches.match('/app/offline.html');
742
+ if (!response) {
743
+ throw new Error('/app/offline.html not in cache');
744
+ }
745
+
746
+ const text = await response.text();
747
+ if (!text.includes('offline')) {
748
+ throw new Error('Offline page invalid');
749
+ }
750
+
751
+ log('✓', 'Offline fallback page is cached');
752
+ }));
753
+
754
+ // Test: Operation Queue
755
+ tests.push(new TestRunner('test-7', 'Operation Queue', async () => {
756
+ if (!window.offlineManager.isDBSupported) {
757
+ throw new Error('IndexedDB not supported');
758
+ }
759
+
760
+ const result = await window.offlineManager.queueOperation({
761
+ type: 'test_op',
762
+ timestamp: Date.now()
763
+ });
764
+
765
+ log('✓', `Operation queued: ${result}`);
766
+ }));
767
+
768
+ // Test: Cache-First Strategy
769
+ tests.push(new TestRunner('test-8', 'Cache-First Strategy', async () => {
770
+ const cache = await caches.open('cyclecad-static-v3');
771
+ const keys = await cache.keys();
772
+
773
+ if (keys.length === 0) {
774
+ throw new Error('No static files cached');
775
+ }
776
+
777
+ log('✓', `Static cache contains ${keys.length} files`);
778
+ }));
779
+
780
+ // Test: Network-First Strategy
781
+ tests.push(new TestRunner('test-9', 'Network-First Strategy', async () => {
782
+ const cache = await caches.open('cyclecad-api-v3');
783
+ if (!cache) {
784
+ // API cache may not exist if no API calls made
785
+ log('ℹ', 'API cache not yet created (no API calls made)');
786
+ } else {
787
+ const keys = await cache.keys();
788
+ log('✓', `API cache contains ${keys.length} entries`);
789
+ }
790
+ }));
791
+
792
+ // Test: Model Cache Size
793
+ tests.push(new TestRunner('test-10', 'Model Cache Size', async () => {
794
+ const cache = await caches.open('cyclecad-models-v3');
795
+ if (!cache) {
796
+ log('ℹ', 'Model cache not created yet');
797
+ return;
798
+ }
799
+
800
+ let size = 0;
801
+ const keys = await cache.keys();
802
+
803
+ for (const request of keys) {
804
+ const response = await cache.match(request);
805
+ if (response) {
806
+ const contentLength = response.headers.get('content-length');
807
+ size += parseInt(contentLength || 0);
808
+ }
809
+ }
810
+
811
+ log('✓', `Model cache size: ${formatBytes(size)}`);
812
+ }));
813
+
814
+ // Test: Cache Versioning
815
+ tests.push(new TestRunner('test-11', 'Cache Versioning', async () => {
816
+ const cacheNames = await caches.keys();
817
+ const cyclecadCaches = cacheNames.filter(name => name.includes('cyclecad'));
818
+
819
+ if (cyclecadCaches.length === 0) {
820
+ throw new Error('No cycleCAD caches found');
821
+ }
822
+
823
+ log('✓', `Active caches: ${cyclecadCaches.join(', ')}`);
824
+ }));
825
+
826
+ // Test: LRU Eviction
827
+ tests.push(new TestRunner('test-12', 'LRU Eviction', async () => {
828
+ log('ℹ', 'LRU tested automatically when model cache > 500MB');
829
+ }));
830
+
831
+ // Test: Cache Persistence
832
+ tests.push(new TestRunner('test-13', 'Cache Persistence', async () => {
833
+ const before = await caches.keys();
834
+ log('✓', `Cache persists across reloads. Current caches: ${before.length}`);
835
+ }));
836
+
837
+ // Test: IndexedDB Available
838
+ tests.push(new TestRunner('test-14', 'IndexedDB Available', async () => {
839
+ if (!('indexedDB' in window)) {
840
+ throw new Error('IndexedDB not available');
841
+ }
842
+
843
+ const db = new Promise((resolve, reject) => {
844
+ const req = indexedDB.open('cyclecad', 1);
845
+ req.onerror = () => reject(req.error);
846
+ req.onsuccess = () => resolve(req.result);
847
+ });
848
+
849
+ await db;
850
+ log('✓', 'IndexedDB available and working');
851
+ }));
852
+
853
+ // Test: Project Store
854
+ tests.push(new TestRunner('test-15', 'Project Store', async () => {
855
+ const db = await new Promise((resolve, reject) => {
856
+ const req = indexedDB.open('cyclecad', 1);
857
+ req.onerror = () => reject(req.error);
858
+ req.onsuccess = () => resolve(req.result);
859
+ });
860
+
861
+ const tx = db.transaction('projects', 'readwrite');
862
+ const store = tx.objectStore('projects');
863
+
864
+ await new Promise((resolve, reject) => {
865
+ const req = store.add({
866
+ id: 'test-' + Date.now(),
867
+ name: 'Test Project',
868
+ created: Date.now()
869
+ });
870
+ req.onerror = () => reject(req.error);
871
+ req.onsuccess = () => resolve();
872
+ });
873
+
874
+ log('✓', 'Can store and retrieve projects');
875
+ }));
876
+
877
+ // Test: Operation Queue Store
878
+ tests.push(new TestRunner('test-16', 'Operation Queue Store', async () => {
879
+ const db = await new Promise((resolve, reject) => {
880
+ const req = indexedDB.open('cyclecad', 1);
881
+ req.onerror = () => reject(req.error);
882
+ req.onsuccess = () => resolve(req.result);
883
+ });
884
+
885
+ const tx = db.transaction('operationQueue', 'readwrite');
886
+ const store = tx.objectStore('operationQueue');
887
+
888
+ const id = await new Promise((resolve, reject) => {
889
+ const req = store.add({
890
+ timestamp: Date.now(),
891
+ data: { type: 'test' }
892
+ });
893
+ req.onerror = () => reject(req.error);
894
+ req.onsuccess = () => resolve(req.result);
895
+ });
896
+
897
+ log('✓', `Operation queue working. Added record: ${id}`);
898
+ }));
899
+
900
+ // Test: Storage Quota
901
+ tests.push(new TestRunner('test-17', 'Storage Quota', async () => {
902
+ if (!navigator.storage || !navigator.storage.estimate) {
903
+ throw new Error('Storage quota API not available');
904
+ }
905
+
906
+ const estimate = await navigator.storage.estimate();
907
+ const used = formatBytes(estimate.usage);
908
+ const quota = formatBytes(estimate.quota);
909
+ const percent = (estimate.usage / estimate.quota * 100).toFixed(1);
910
+
911
+ log('✓', `Using ${used} of ${quota} (${percent}%)`);
912
+ }));
913
+
914
+ // Test: Queue Operation
915
+ tests.push(new TestRunner('test-18', 'Queue Operation', async () => {
916
+ const result = await window.offlineManager.queueOperation({
917
+ type: 'test_operation',
918
+ data: { test: true }
919
+ });
920
+
921
+ log('✓', `Operation queued successfully`);
922
+ }));
923
+
924
+ // Test: Sync Operations
925
+ tests.push(new TestRunner('test-19', 'Sync Operations', async () => {
926
+ log('ℹ', 'Sync test requires actual offline workflow. Test manually by:');
927
+ log('ℹ', '1. Go offline (F12 > Network)');
928
+ log('ℹ', '2. Create/modify a project');
929
+ log('ℹ', '3. Go online');
930
+ log('ℹ', '4. Watch for "Syncing..." notification');
931
+ }));
932
+
933
+ // Test: Sync Order
934
+ tests.push(new TestRunner('test-20', 'Sync Order', async () => {
935
+ log('ℹ', 'Operations sync in creation order (FIFO)');
936
+ }));
937
+
938
+ // Test: Sync Progress
939
+ tests.push(new TestRunner('test-21', 'Sync Progress', async () => {
940
+ log('ℹ', 'Progress shown in bottom-left notification');
941
+ }));
942
+
943
+ // Test: Manifest Valid
944
+ tests.push(new TestRunner('test-22', 'Manifest Valid', async () => {
945
+ const response = await fetch('/app/manifest.json');
946
+ const manifest = await response.json();
947
+
948
+ if (!manifest || typeof manifest !== 'object') {
949
+ throw new Error('Invalid manifest');
950
+ }
951
+
952
+ log('✓', `Manifest loaded: ${manifest.name}`);
953
+ }));
954
+
955
+ // Test: Manifest Required Fields
956
+ tests.push(new TestRunner('test-23', 'Manifest Required Fields', async () => {
957
+ const response = await fetch('/app/manifest.json');
958
+ const manifest = await response.json();
959
+
960
+ const required = ['name', 'short_name', 'start_url', 'display', 'icons'];
961
+ const missing = required.filter(field => !manifest[field]);
962
+
963
+ if (missing.length > 0) {
964
+ throw new Error(`Missing fields: ${missing.join(', ')}`);
965
+ }
966
+
967
+ log('✓', `All required fields present`);
968
+ }));
969
+
970
+ // Test: Start URL
971
+ tests.push(new TestRunner('test-24', 'Start URL', async () => {
972
+ const response = await fetch('/app/manifest.json');
973
+ const manifest = await response.json();
974
+
975
+ const startUrl = manifest.start_url;
976
+ const urlResponse = await fetch(startUrl);
977
+
978
+ if (!urlResponse.ok) {
979
+ throw new Error(`Start URL not accessible: ${startUrl}`);
980
+ }
981
+
982
+ log('✓', `Start URL accessible: ${startUrl}`);
983
+ }));
984
+
985
+ // Test: Icons Accessible
986
+ tests.push(new TestRunner('test-25', 'Icons Accessible', async () => {
987
+ const response = await fetch('/app/manifest.json');
988
+ const manifest = await response.json();
989
+
990
+ if (!manifest.icons || manifest.icons.length === 0) {
991
+ throw new Error('No icons in manifest');
992
+ }
993
+
994
+ log('✓', `${manifest.icons.length} icon sizes defined`);
995
+ }));
996
+
997
+ // Test: Install Prompt
998
+ tests.push(new TestRunner('test-26', 'Install Prompt', async () => {
999
+ log('ℹ', 'Install prompt shown automatically on first visit');
1000
+ log('ℹ', 'Also available in browser menu → Install');
1001
+ }));
1002
+
1003
+ // Test: Update Detection
1004
+ tests.push(new TestRunner('test-27', 'Update Detection', async () => {
1005
+ if (!('serviceWorker' in navigator)) {
1006
+ throw new Error('Service Worker not available');
1007
+ }
1008
+
1009
+ const reg = await navigator.serviceWorker.getRegistration('/app/');
1010
+ if (!reg) {
1011
+ throw new Error('Service Worker not registered');
1012
+ }
1013
+
1014
+ // Manually check for updates
1015
+ await reg.update();
1016
+ log('✓', 'Update check performed (check console for results)');
1017
+ }));
1018
+
1019
+ // Test: HTTPS Requirement
1020
+ tests.push(new TestRunner('test-28', 'HTTPS Requirement', async () => {
1021
+ const protocol = window.location.protocol;
1022
+
1023
+ if (protocol !== 'https:' && !window.location.hostname.includes('localhost')) {
1024
+ throw new Error(`PWA requires HTTPS. Current: ${protocol}`);
1025
+ }
1026
+
1027
+ log('✓', `Using secure protocol: ${protocol}`);
1028
+ }));
1029
+
1030
+ // Helper: Update test status
1031
+ function updateStatus(testId, status, result = '') {
1032
+ const statusEl = document.getElementById(`${testId}-status`);
1033
+ const statusClass = `status-${status}`;
1034
+
1035
+ statusEl.textContent = status;
1036
+ statusEl.className = `test-status ${statusClass}`;
1037
+
1038
+ if (result) {
1039
+ log(getIcon(status), result);
1040
+ }
1041
+
1042
+ updateStats();
1043
+ updateProgress();
1044
+ }
1045
+
1046
+ // Helper: Log result
1047
+ function log(icon, message) {
1048
+ const panel = document.getElementById('results-panel');
1049
+
1050
+ if (panel.textContent.includes('No tests run')) {
1051
+ panel.innerHTML = '<div class="results-header">Test Results</div>';
1052
+ }
1053
+
1054
+ const className = icon === '✓' ? 'pass' : icon === '✗' ? 'fail' : 'info';
1055
+ const item = document.createElement('div');
1056
+ item.className = `result-item ${className}`;
1057
+ item.innerHTML = `<span class="result-icon">${icon}</span> <span>${message}</span>`;
1058
+
1059
+ panel.appendChild(item);
1060
+ panel.scrollTop = panel.scrollHeight;
1061
+ }
1062
+
1063
+ // Helper: Get icon for status
1064
+ function getIcon(status) {
1065
+ return {
1066
+ 'pass': '✓',
1067
+ 'fail': '✗',
1068
+ 'running': '⟳',
1069
+ 'pending': '◯'
1070
+ }[status] || '?';
1071
+ }
1072
+
1073
+ // Helper: Format bytes
1074
+ function formatBytes(bytes) {
1075
+ if (bytes === 0) return '0 B';
1076
+ const k = 1024;
1077
+ const sizes = ['B', 'KB', 'MB', 'GB'];
1078
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1079
+ return Math.round(bytes / Math.pow(k, i) * 10) / 10 + ' ' + sizes[i];
1080
+ }
1081
+
1082
+ // Update stats
1083
+ function updateStats() {
1084
+ totalCount = tests.length;
1085
+ document.getElementById('total-count').textContent = totalCount;
1086
+ document.getElementById('pass-count').textContent = passCount;
1087
+ document.getElementById('fail-count').textContent = failCount;
1088
+ document.getElementById('run-count').textContent = totalCount - passCount - failCount;
1089
+ }
1090
+
1091
+ // Update progress
1092
+ function updateProgress() {
1093
+ const completed = passCount + failCount;
1094
+ const pct = (completed / totalCount) * 100;
1095
+ document.getElementById('progress-fill').style.width = pct + '%';
1096
+ }
1097
+
1098
+ // Run single test
1099
+ async function runTest(testId) {
1100
+ const idx = parseInt(testId.replace('test-', '')) - 1;
1101
+ if (tests[idx]) {
1102
+ await tests[idx].run();
1103
+ }
1104
+ }
1105
+
1106
+ // Run all tests
1107
+ async function runAllTests() {
1108
+ passCount = 0;
1109
+ failCount = 0;
1110
+ document.getElementById('results-panel').innerHTML = '<div class="results-header">Test Results</div>';
1111
+
1112
+ for (const test of tests) {
1113
+ await test.run();
1114
+ await new Promise(resolve => setTimeout(resolve, 100));
1115
+ }
1116
+
1117
+ log('✓', `Test run complete: ${passCount} passed, ${failCount} failed`);
1118
+ }
1119
+
1120
+ // Clear results
1121
+ function clearResults() {
1122
+ passCount = 0;
1123
+ failCount = 0;
1124
+ tests.forEach(test => test.status = 'pending');
1125
+ document.getElementById('results-panel').innerHTML = '<div class="results-header">Test Results</div><p style="color: #9ca3af; font-size: 13px;">No tests run yet. Click "Run All Tests" to begin.</p>';
1126
+ updateStats();
1127
+ updateProgress();
1128
+ }
1129
+
1130
+ // Initialize
1131
+ updateStats();
1132
+ </script>
1133
+ </body>
1134
+ </html>