cyclecad 3.0.0 → 3.2.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 (67) 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/DOCKER-BUILD-MANIFEST.txt +483 -0
  7. package/DOCKER-FILES-REFERENCE.md +440 -0
  8. package/DOCKER-INFRASTRUCTURE.md +475 -0
  9. package/DOCKER-README.md +435 -0
  10. package/Dockerfile +33 -55
  11. package/PWA-FILES-CREATED.txt +350 -0
  12. package/QUICK-START-TESTING.md +126 -0
  13. package/STEP-IMPORT-QUICKSTART.md +347 -0
  14. package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
  15. package/app/css/mobile.css +1074 -0
  16. package/app/icons/generate-icons.js +203 -0
  17. package/app/index.html +93 -0
  18. package/app/js/billing-ui.js +990 -0
  19. package/app/js/brep-kernel.js +933 -981
  20. package/app/js/collab-client.js +750 -0
  21. package/app/js/mobile-nav.js +623 -0
  22. package/app/js/mobile-toolbar.js +476 -0
  23. package/app/js/modules/billing-module.js +724 -0
  24. package/app/js/modules/step-module-enhanced.js +938 -0
  25. package/app/js/offline-manager.js +705 -0
  26. package/app/js/responsive-init.js +360 -0
  27. package/app/js/touch-handler.js +429 -0
  28. package/app/manifest.json +211 -0
  29. package/app/offline.html +508 -0
  30. package/app/sw.js +571 -0
  31. package/app/tests/billing-tests.html +779 -0
  32. package/app/tests/brep-tests.html +980 -0
  33. package/app/tests/collab-tests.html +743 -0
  34. package/app/tests/mobile-tests.html +1299 -0
  35. package/app/tests/pwa-tests.html +1134 -0
  36. package/app/tests/step-tests.html +1042 -0
  37. package/app/tests/test-agent-v3.html +719 -0
  38. package/docker-compose.yml +225 -0
  39. package/docs/BILLING-HELP.json +260 -0
  40. package/docs/BILLING-README.md +639 -0
  41. package/docs/BILLING-TUTORIAL.md +736 -0
  42. package/docs/BREP-HELP.json +326 -0
  43. package/docs/BREP-TUTORIAL.md +802 -0
  44. package/docs/COLLABORATION-HELP.json +228 -0
  45. package/docs/COLLABORATION-TUTORIAL.md +818 -0
  46. package/docs/DOCKER-HELP.json +224 -0
  47. package/docs/DOCKER-TUTORIAL.md +974 -0
  48. package/docs/MOBILE-HELP.json +243 -0
  49. package/docs/MOBILE-RESPONSIVE-README.md +378 -0
  50. package/docs/MOBILE-TUTORIAL.md +747 -0
  51. package/docs/PWA-HELP.json +228 -0
  52. package/docs/PWA-README.md +662 -0
  53. package/docs/PWA-TUTORIAL.md +757 -0
  54. package/docs/STEP-HELP.json +481 -0
  55. package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
  56. package/docs/TESTING-GUIDE.md +528 -0
  57. package/docs/TESTING-HELP.json +182 -0
  58. package/fusion-vs-cyclecad.html +1771 -0
  59. package/nginx.conf +237 -0
  60. package/package.json +1 -1
  61. package/server/Dockerfile.converter +51 -0
  62. package/server/Dockerfile.signaling +28 -0
  63. package/server/billing-server.js +487 -0
  64. package/server/converter-enhanced.py +528 -0
  65. package/server/requirements-converter.txt +29 -0
  66. package/server/signaling-server.js +801 -0
  67. package/tests/docker-tests.sh +389 -0
package/app/sw.js ADDED
@@ -0,0 +1,571 @@
1
+ /**
2
+ * cycleCAD Service Worker
3
+ * Enables offline mode, caching strategies, and background sync
4
+ * v3.0.0
5
+ */
6
+
7
+ const CACHE_VERSION = 'cyclecad-v3.0.0';
8
+ const STATIC_CACHE = 'cyclecad-static-v3';
9
+ const DYNAMIC_CACHE = 'cyclecad-dynamic-v3';
10
+ const MODEL_CACHE = 'cyclecad-models-v3';
11
+ const API_CACHE = 'cyclecad-api-v3';
12
+
13
+ // Essential files to precache on install
14
+ const PRECACHE_URLS = [
15
+ '/app/',
16
+ '/app/index.html',
17
+ '/app/offline.html',
18
+ '/app/manifest.json',
19
+ '/app/js/app.js',
20
+ '/app/js/viewport.js',
21
+ '/app/js/sketch.js',
22
+ '/app/js/operations.js',
23
+ '/app/js/constraint-solver.js',
24
+ '/app/js/advanced-ops.js',
25
+ '/app/js/assembly.js',
26
+ '/app/js/dxf-export.js',
27
+ '/app/js/export.js',
28
+ '/app/js/params.js',
29
+ '/app/js/tree.js',
30
+ '/app/js/shortcuts.js',
31
+ '/app/css/style.css',
32
+ '/app/offline.html'
33
+ ];
34
+
35
+ // CDN resources to cache (long TTL)
36
+ const CDN_PATTERNS = [
37
+ 'cdn.jsdelivr.net',
38
+ 'unpkg.com',
39
+ 'cdnjs.cloudflare.com'
40
+ ];
41
+
42
+ // API endpoints — network-first with fallback
43
+ const API_PATTERNS = [
44
+ '/api/',
45
+ '/convert',
46
+ '/health'
47
+ ];
48
+
49
+ // Model files — cache with size limit
50
+ const MODEL_PATTERNS = [
51
+ /\.glb$/i,
52
+ /\.gltf$/i,
53
+ /\.step$/i,
54
+ /\.stp$/i,
55
+ /\.stl$/i,
56
+ /\.obj$/i
57
+ ];
58
+
59
+ // Max size for model cache (500MB)
60
+ const MODEL_CACHE_MAX_SIZE = 500 * 1024 * 1024;
61
+
62
+ /**
63
+ * INSTALL EVENT
64
+ * Precache all essential files
65
+ */
66
+ self.addEventListener('install', (event) => {
67
+ console.log('[SW] Install event, precaching essential files...');
68
+
69
+ event.waitUntil(
70
+ caches.open(STATIC_CACHE)
71
+ .then((cache) => {
72
+ console.log('[SW] Precaching', PRECACHE_URLS.length, 'files');
73
+ return cache.addAll(PRECACHE_URLS);
74
+ })
75
+ .then(() => {
76
+ console.log('[SW] Precache complete, skipping wait...');
77
+ return self.skipWaiting();
78
+ })
79
+ .catch((err) => {
80
+ console.error('[SW] Precache failed:', err);
81
+ })
82
+ );
83
+ });
84
+
85
+ /**
86
+ * ACTIVATE EVENT
87
+ * Clean up old caches
88
+ */
89
+ self.addEventListener('activate', (event) => {
90
+ console.log('[SW] Activate event, cleaning old caches...');
91
+
92
+ event.waitUntil(
93
+ caches.keys()
94
+ .then((cacheNames) => {
95
+ return Promise.all(
96
+ cacheNames.map((cacheName) => {
97
+ if (cacheName !== STATIC_CACHE &&
98
+ cacheName !== DYNAMIC_CACHE &&
99
+ cacheName !== MODEL_CACHE &&
100
+ cacheName !== API_CACHE &&
101
+ !cacheName.startsWith('cyclecad-')) {
102
+ console.log('[SW] Deleting old cache:', cacheName);
103
+ return caches.delete(cacheName);
104
+ }
105
+ })
106
+ );
107
+ })
108
+ .then(() => {
109
+ console.log('[SW] Old caches cleaned, claiming clients...');
110
+ return self.clients.claim();
111
+ })
112
+ );
113
+ });
114
+
115
+ /**
116
+ * FETCH EVENT
117
+ * Routing based on URL pattern
118
+ */
119
+ self.addEventListener('fetch', (event) => {
120
+ const { request } = event;
121
+ const url = new URL(request.url);
122
+
123
+ // Skip non-GET requests
124
+ if (request.method !== 'GET') {
125
+ return;
126
+ }
127
+
128
+ // Skip chrome extensions and internal protocols
129
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
130
+ return;
131
+ }
132
+
133
+ // API calls — network-first with cache fallback
134
+ if (isApiCall(url.pathname)) {
135
+ event.respondWith(networkFirstApiCall(request));
136
+ return;
137
+ }
138
+
139
+ // Model files — cache-first with size management
140
+ if (isModelFile(url.pathname)) {
141
+ event.respondWith(cacheFirstModel(request));
142
+ return;
143
+ }
144
+
145
+ // CDN resources — cache-first with long TTL
146
+ if (isCdnResource(url.hostname)) {
147
+ event.respondWith(cacheFirstCdn(request));
148
+ return;
149
+ }
150
+
151
+ // Static assets (HTML, JS, CSS) — cache-first, update in background
152
+ if (isStaticAsset(url.pathname)) {
153
+ event.respondWith(cacheFirstStatic(request));
154
+ return;
155
+ }
156
+
157
+ // Default — network-first with offline fallback
158
+ event.respondWith(networkFirstWithFallback(request));
159
+ });
160
+
161
+ /**
162
+ * BACKGROUND SYNC
163
+ * Sync queued operations when back online
164
+ */
165
+ self.addEventListener('sync', (event) => {
166
+ console.log('[SW] Background sync event:', event.tag);
167
+
168
+ if (event.tag === 'sync-operations') {
169
+ event.waitUntil(syncOfflineOperations());
170
+ }
171
+ });
172
+
173
+ /**
174
+ * HELPER: API Call — Network-first
175
+ */
176
+ async function networkFirstApiCall(request) {
177
+ try {
178
+ const response = await fetch(request);
179
+
180
+ // Cache successful responses
181
+ if (response.ok) {
182
+ const cache = await caches.open(API_CACHE);
183
+ cache.put(request, response.clone());
184
+ }
185
+
186
+ return response;
187
+ } catch (err) {
188
+ console.log('[SW] Network failed, falling back to cache for:', request.url);
189
+ const cached = await caches.match(request);
190
+
191
+ if (cached) {
192
+ return cached;
193
+ }
194
+
195
+ // Return offline response
196
+ return new Response(
197
+ JSON.stringify({
198
+ error: 'offline',
199
+ message: 'API unavailable. Changes will sync when online.'
200
+ }),
201
+ {
202
+ status: 503,
203
+ statusText: 'Service Unavailable',
204
+ headers: { 'Content-Type': 'application/json' }
205
+ }
206
+ );
207
+ }
208
+ }
209
+
210
+ /**
211
+ * HELPER: Model Files — Cache-first with LRU eviction
212
+ */
213
+ async function cacheFirstModel(request) {
214
+ const cache = await caches.open(MODEL_CACHE);
215
+ const cached = await cache.match(request);
216
+
217
+ if (cached) {
218
+ console.log('[SW] Model cache hit:', request.url);
219
+ return cached;
220
+ }
221
+
222
+ try {
223
+ const response = await fetch(request);
224
+
225
+ if (response.ok) {
226
+ // Check cache size before adding
227
+ const size = parseInt(response.headers.get('content-length') || 0);
228
+
229
+ if (size > 0) {
230
+ await enforceModelCacheSize(size);
231
+ cache.put(request, response.clone());
232
+ }
233
+ }
234
+
235
+ return response;
236
+ } catch (err) {
237
+ console.log('[SW] Model fetch failed:', request.url);
238
+
239
+ // Try to return from cache anyway
240
+ const cached = await cache.match(request);
241
+ if (cached) return cached;
242
+
243
+ // Return offline response
244
+ return new Response(
245
+ 'Model file not available offline',
246
+ { status: 503 }
247
+ );
248
+ }
249
+ }
250
+
251
+ /**
252
+ * HELPER: CDN Resources — Cache-first with long TTL
253
+ */
254
+ async function cacheFirstCdn(request) {
255
+ const cache = await caches.open(STATIC_CACHE);
256
+ const cached = await cache.match(request);
257
+
258
+ if (cached) {
259
+ console.log('[SW] CDN cache hit:', request.url);
260
+ return cached;
261
+ }
262
+
263
+ try {
264
+ const response = await fetch(request);
265
+
266
+ if (response.ok) {
267
+ cache.put(request, response.clone());
268
+ }
269
+
270
+ return response;
271
+ } catch (err) {
272
+ console.log('[SW] CDN fetch failed:', request.url);
273
+
274
+ const cached = await cache.match(request);
275
+ if (cached) return cached;
276
+
277
+ return new Response('CDN resource unavailable', { status: 503 });
278
+ }
279
+ }
280
+
281
+ /**
282
+ * HELPER: Static Assets — Cache-first with background update
283
+ */
284
+ async function cacheFirstStatic(request) {
285
+ const cache = await caches.open(STATIC_CACHE);
286
+ const cached = await cache.match(request);
287
+
288
+ if (cached) {
289
+ console.log('[SW] Static cache hit:', request.url);
290
+
291
+ // Update in background (don't block response)
292
+ fetch(request)
293
+ .then((response) => {
294
+ if (response.ok) {
295
+ cache.put(request, response.clone());
296
+ console.log('[SW] Updated static file:', request.url);
297
+
298
+ // Notify clients of update
299
+ notifyClientsOfUpdate();
300
+ }
301
+ })
302
+ .catch((err) => console.log('[SW] Background update failed:', err));
303
+
304
+ return cached;
305
+ }
306
+
307
+ try {
308
+ const response = await fetch(request);
309
+
310
+ if (response.ok) {
311
+ cache.put(request, response.clone());
312
+ }
313
+
314
+ return response;
315
+ } catch (err) {
316
+ console.log('[SW] Static fetch failed:', request.url);
317
+
318
+ // Return offline page for HTML requests
319
+ if (request.destination === 'document' || request.headers.get('accept')?.includes('text/html')) {
320
+ const offline = await cache.match('/app/offline.html');
321
+ if (offline) return offline;
322
+ }
323
+
324
+ return new Response('Offline', { status: 503 });
325
+ }
326
+ }
327
+
328
+ /**
329
+ * HELPER: Network-first with offline fallback
330
+ */
331
+ async function networkFirstWithFallback(request) {
332
+ try {
333
+ const response = await fetch(request);
334
+
335
+ if (response.ok) {
336
+ const cache = await caches.open(DYNAMIC_CACHE);
337
+ cache.put(request, response.clone());
338
+ }
339
+
340
+ return response;
341
+ } catch (err) {
342
+ console.log('[SW] Network failed, checking cache:', request.url);
343
+
344
+ // Try dynamic cache
345
+ const cached = await caches.match(request);
346
+ if (cached) return cached;
347
+
348
+ // Try static cache as last resort
349
+ const staticCached = await caches.match(request, { cacheName: STATIC_CACHE });
350
+ if (staticCached) return staticCached;
351
+
352
+ // Return offline page for documents
353
+ if (request.destination === 'document') {
354
+ return caches.match('/app/offline.html');
355
+ }
356
+
357
+ return new Response('Offline', { status: 503 });
358
+ }
359
+ }
360
+
361
+ /**
362
+ * HELPER: Check if URL is an API call
363
+ */
364
+ function isApiCall(pathname) {
365
+ return API_PATTERNS.some((pattern) => {
366
+ if (typeof pattern === 'string') {
367
+ return pathname.includes(pattern);
368
+ }
369
+ return pattern.test(pathname);
370
+ });
371
+ }
372
+
373
+ /**
374
+ * HELPER: Check if URL is a model file
375
+ */
376
+ function isModelFile(pathname) {
377
+ return MODEL_PATTERNS.some((pattern) => pattern.test(pathname));
378
+ }
379
+
380
+ /**
381
+ * HELPER: Check if hostname is CDN
382
+ */
383
+ function isCdnResource(hostname) {
384
+ return CDN_PATTERNS.some((cdn) => hostname.includes(cdn));
385
+ }
386
+
387
+ /**
388
+ * HELPER: Check if pathname is static asset
389
+ */
390
+ function isStaticAsset(pathname) {
391
+ return /\.(js|css|svg|png|jpg|jpeg|gif|woff|woff2|ttf|eot)$/i.test(pathname) ||
392
+ pathname.endsWith('/app/') ||
393
+ pathname.endsWith('/app/index.html');
394
+ }
395
+
396
+ /**
397
+ * HELPER: Enforce model cache size limit (LRU eviction)
398
+ */
399
+ async function enforceModelCacheSize(newSize) {
400
+ const cache = await caches.open(MODEL_CACHE);
401
+ const keys = await cache.keys();
402
+
403
+ let totalSize = newSize;
404
+
405
+ for (const request of keys) {
406
+ const response = await cache.match(request);
407
+ const size = parseInt(response.headers.get('content-length') || 0);
408
+ totalSize += size;
409
+ }
410
+
411
+ if (totalSize > MODEL_CACHE_MAX_SIZE) {
412
+ console.log('[SW] Model cache exceeds limit, evicting oldest files...');
413
+
414
+ for (const request of keys) {
415
+ const response = await cache.match(request);
416
+ const size = parseInt(response.headers.get('content-length') || 0);
417
+
418
+ await cache.delete(request);
419
+ totalSize -= size;
420
+
421
+ if (totalSize <= MODEL_CACHE_MAX_SIZE * 0.8) {
422
+ console.log('[SW] Cache size reduced to', Math.round(totalSize / 1024 / 1024), 'MB');
423
+ break;
424
+ }
425
+ }
426
+ }
427
+ }
428
+
429
+ /**
430
+ * HELPER: Sync offline operations when back online
431
+ */
432
+ async function syncOfflineOperations() {
433
+ try {
434
+ // Get queued operations from IndexedDB
435
+ const db = await openDatabase();
436
+ const tx = db.transaction('operationQueue', 'readonly');
437
+ const store = tx.objectStore('operationQueue');
438
+ const operations = await store.getAll();
439
+
440
+ console.log('[SW] Syncing', operations.length, 'queued operations...');
441
+
442
+ for (const op of operations) {
443
+ try {
444
+ const response = await fetch('/api/operations', {
445
+ method: 'POST',
446
+ headers: { 'Content-Type': 'application/json' },
447
+ body: JSON.stringify(op.data)
448
+ });
449
+
450
+ if (response.ok) {
451
+ // Remove from queue
452
+ const txW = db.transaction('operationQueue', 'readwrite');
453
+ await txW.objectStore('operationQueue').delete(op.id);
454
+ console.log('[SW] Synced operation:', op.id);
455
+ }
456
+ } catch (err) {
457
+ console.error('[SW] Sync failed for operation:', op.id, err);
458
+ // Leave in queue for next sync
459
+ }
460
+ }
461
+
462
+ // Notify clients of sync completion
463
+ notifyClientsOfSync();
464
+
465
+ } catch (err) {
466
+ console.error('[SW] Sync failed:', err);
467
+ throw err;
468
+ }
469
+ }
470
+
471
+ /**
472
+ * HELPER: Open IndexedDB
473
+ */
474
+ function openDatabase() {
475
+ return new Promise((resolve, reject) => {
476
+ const request = indexedDB.open('cyclecad', 1);
477
+
478
+ request.onerror = () => reject(request.error);
479
+ request.onsuccess = () => resolve(request.result);
480
+
481
+ request.onupgradeneeded = (event) => {
482
+ const db = event.target.result;
483
+ if (!db.objectStoreNames.contains('operationQueue')) {
484
+ db.createObjectStore('operationQueue', { keyPath: 'id' });
485
+ }
486
+ };
487
+ });
488
+ }
489
+
490
+ /**
491
+ * HELPER: Notify all clients of update available
492
+ */
493
+ function notifyClientsOfUpdate() {
494
+ self.clients.matchAll().then((clients) => {
495
+ clients.forEach((client) => {
496
+ client.postMessage({
497
+ type: 'UPDATE_AVAILABLE',
498
+ message: 'A new version of cycleCAD is available. Reload to update.'
499
+ });
500
+ });
501
+ });
502
+ }
503
+
504
+ /**
505
+ * HELPER: Notify all clients of sync completion
506
+ */
507
+ function notifyClientsOfSync() {
508
+ self.clients.matchAll().then((clients) => {
509
+ clients.forEach((client) => {
510
+ client.postMessage({
511
+ type: 'SYNC_COMPLETE',
512
+ message: 'Offline changes have been synced.'
513
+ });
514
+ });
515
+ });
516
+ }
517
+
518
+ /**
519
+ * MESSAGE EVENT
520
+ * Handle messages from clients
521
+ */
522
+ self.addEventListener('message', (event) => {
523
+ console.log('[SW] Message received:', event.data);
524
+
525
+ if (event.data.type === 'SKIP_WAITING') {
526
+ self.skipWaiting();
527
+ }
528
+
529
+ if (event.data.type === 'CLEAR_CACHE') {
530
+ clearAllCaches().then(() => {
531
+ event.ports[0].postMessage({ success: true });
532
+ });
533
+ }
534
+
535
+ if (event.data.type === 'GET_CACHE_SIZE') {
536
+ getCacheSize().then((size) => {
537
+ event.ports[0].postMessage({ size });
538
+ });
539
+ }
540
+ });
541
+
542
+ /**
543
+ * HELPER: Clear all caches
544
+ */
545
+ async function clearAllCaches() {
546
+ const cacheNames = await caches.keys();
547
+ return Promise.all(cacheNames.map((name) => caches.delete(name)));
548
+ }
549
+
550
+ /**
551
+ * HELPER: Get total cache size
552
+ */
553
+ async function getCacheSize() {
554
+ const cacheNames = await caches.keys();
555
+ let totalSize = 0;
556
+
557
+ for (const name of cacheNames) {
558
+ const cache = await caches.open(name);
559
+ const keys = await cache.keys();
560
+
561
+ for (const request of keys) {
562
+ const response = await cache.match(request);
563
+ const size = parseInt(response.headers.get('content-length') || 0);
564
+ totalSize += size;
565
+ }
566
+ }
567
+
568
+ return totalSize;
569
+ }
570
+
571
+ console.log('[SW] Service Worker loaded and ready');