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.
- package/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
- package/BILLING-INDEX.md +293 -0
- package/BILLING-INTEGRATION-GUIDE.md +414 -0
- package/COLLABORATION-INDEX.md +440 -0
- package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
- package/DOCKER-BUILD-MANIFEST.txt +483 -0
- package/DOCKER-FILES-REFERENCE.md +440 -0
- package/DOCKER-INFRASTRUCTURE.md +475 -0
- package/DOCKER-README.md +435 -0
- package/Dockerfile +33 -55
- package/PWA-FILES-CREATED.txt +350 -0
- package/QUICK-START-TESTING.md +126 -0
- package/STEP-IMPORT-QUICKSTART.md +347 -0
- package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
- package/app/css/mobile.css +1074 -0
- package/app/icons/generate-icons.js +203 -0
- package/app/index.html +93 -0
- package/app/js/billing-ui.js +990 -0
- package/app/js/brep-kernel.js +933 -981
- package/app/js/collab-client.js +750 -0
- package/app/js/mobile-nav.js +623 -0
- package/app/js/mobile-toolbar.js +476 -0
- package/app/js/modules/billing-module.js +724 -0
- package/app/js/modules/step-module-enhanced.js +938 -0
- package/app/js/offline-manager.js +705 -0
- package/app/js/responsive-init.js +360 -0
- package/app/js/touch-handler.js +429 -0
- package/app/manifest.json +211 -0
- package/app/offline.html +508 -0
- package/app/sw.js +571 -0
- package/app/tests/billing-tests.html +779 -0
- package/app/tests/brep-tests.html +980 -0
- package/app/tests/collab-tests.html +743 -0
- package/app/tests/mobile-tests.html +1299 -0
- package/app/tests/pwa-tests.html +1134 -0
- package/app/tests/step-tests.html +1042 -0
- package/app/tests/test-agent-v3.html +719 -0
- package/docker-compose.yml +225 -0
- package/docs/BILLING-HELP.json +260 -0
- package/docs/BILLING-README.md +639 -0
- package/docs/BILLING-TUTORIAL.md +736 -0
- package/docs/BREP-HELP.json +326 -0
- package/docs/BREP-TUTORIAL.md +802 -0
- package/docs/COLLABORATION-HELP.json +228 -0
- package/docs/COLLABORATION-TUTORIAL.md +818 -0
- package/docs/DOCKER-HELP.json +224 -0
- package/docs/DOCKER-TUTORIAL.md +974 -0
- package/docs/MOBILE-HELP.json +243 -0
- package/docs/MOBILE-RESPONSIVE-README.md +378 -0
- package/docs/MOBILE-TUTORIAL.md +747 -0
- package/docs/PWA-HELP.json +228 -0
- package/docs/PWA-README.md +662 -0
- package/docs/PWA-TUTORIAL.md +757 -0
- package/docs/STEP-HELP.json +481 -0
- package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
- package/docs/TESTING-GUIDE.md +528 -0
- package/docs/TESTING-HELP.json +182 -0
- package/fusion-vs-cyclecad.html +1771 -0
- package/nginx.conf +237 -0
- package/package.json +1 -1
- package/server/Dockerfile.converter +51 -0
- package/server/Dockerfile.signaling +28 -0
- package/server/billing-server.js +487 -0
- package/server/converter-enhanced.py +528 -0
- package/server/requirements-converter.txt +29 -0
- package/server/signaling-server.js +801 -0
- package/tests/docker-tests.sh +389 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline Manager for cycleCAD
|
|
3
|
+
* Handles online/offline detection, operation queuing, sync, and PWA features
|
|
4
|
+
* ~400 lines
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class OfflineManager {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.isOnline = navigator.onLine;
|
|
10
|
+
this.isSWSupported = 'serviceWorker' in navigator;
|
|
11
|
+
this.isDBSupported = 'indexedDB' in window;
|
|
12
|
+
this.operationQueue = [];
|
|
13
|
+
this.syncInProgress = false;
|
|
14
|
+
this.updateAvailable = false;
|
|
15
|
+
this.cacheSize = 0;
|
|
16
|
+
|
|
17
|
+
this.init();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initialize offline manager
|
|
22
|
+
*/
|
|
23
|
+
async init() {
|
|
24
|
+
console.log('[OfflineManager] Initializing...');
|
|
25
|
+
|
|
26
|
+
// Register service worker
|
|
27
|
+
if (this.isSWSupported) {
|
|
28
|
+
await this.registerServiceWorker();
|
|
29
|
+
} else {
|
|
30
|
+
console.warn('[OfflineManager] Service Workers not supported');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Setup online/offline listeners
|
|
34
|
+
window.addEventListener('online', () => this.handleOnline());
|
|
35
|
+
window.addEventListener('offline', () => this.handleOffline());
|
|
36
|
+
|
|
37
|
+
// Setup IndexedDB
|
|
38
|
+
if (this.isDBSupported) {
|
|
39
|
+
await this.initializeDatabase();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Request permissions
|
|
43
|
+
this.requestPermissions();
|
|
44
|
+
|
|
45
|
+
// Sync offline operations if online
|
|
46
|
+
if (this.isOnline) {
|
|
47
|
+
await this.syncOperations();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check for updates
|
|
51
|
+
this.checkForUpdates();
|
|
52
|
+
|
|
53
|
+
// Setup UI
|
|
54
|
+
this.setupUI();
|
|
55
|
+
|
|
56
|
+
console.log('[OfflineManager] Ready. Online:', this.isOnline);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Register service worker
|
|
61
|
+
*/
|
|
62
|
+
async registerServiceWorker() {
|
|
63
|
+
try {
|
|
64
|
+
const registration = await navigator.serviceWorker.register('/app/sw.js', {
|
|
65
|
+
scope: '/app/'
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
console.log('[SW] Registered successfully');
|
|
69
|
+
|
|
70
|
+
// Listen for updates
|
|
71
|
+
registration.addEventListener('updatefound', () => {
|
|
72
|
+
const newWorker = registration.installing;
|
|
73
|
+
newWorker.addEventListener('statechange', () => {
|
|
74
|
+
if (newWorker.state === 'activated') {
|
|
75
|
+
this.showUpdatePrompt();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Handle messages from SW
|
|
81
|
+
navigator.serviceWorker.addEventListener('message', (event) => {
|
|
82
|
+
this.handleSWMessage(event.data);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error('[SW] Registration failed:', err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Initialize IndexedDB
|
|
92
|
+
*/
|
|
93
|
+
async initializeDatabase() {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const request = indexedDB.open('cyclecad', 1);
|
|
96
|
+
|
|
97
|
+
request.onerror = () => reject(request.error);
|
|
98
|
+
request.onsuccess = () => {
|
|
99
|
+
this.db = request.result;
|
|
100
|
+
console.log('[DB] Opened successfully');
|
|
101
|
+
resolve();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
request.onupgradeneeded = (event) => {
|
|
105
|
+
const db = event.target.result;
|
|
106
|
+
|
|
107
|
+
// Create object stores if they don't exist
|
|
108
|
+
if (!db.objectStoreNames.contains('operationQueue')) {
|
|
109
|
+
db.createObjectStore('operationQueue', { keyPath: 'id', autoIncrement: true });
|
|
110
|
+
}
|
|
111
|
+
if (!db.objectStoreNames.contains('projects')) {
|
|
112
|
+
db.createObjectStore('projects', { keyPath: 'id' });
|
|
113
|
+
}
|
|
114
|
+
if (!db.objectStoreNames.contains('drafts')) {
|
|
115
|
+
db.createObjectStore('drafts', { keyPath: 'id', autoIncrement: true });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log('[DB] Upgraded successfully');
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Queue an operation for offline sync
|
|
125
|
+
*/
|
|
126
|
+
async queueOperation(operation) {
|
|
127
|
+
if (!this.isOnline && this.isDBSupported) {
|
|
128
|
+
try {
|
|
129
|
+
const tx = this.db.transaction('operationQueue', 'readwrite');
|
|
130
|
+
const store = tx.objectStore('operationQueue');
|
|
131
|
+
|
|
132
|
+
await new Promise((resolve, reject) => {
|
|
133
|
+
const req = store.add({
|
|
134
|
+
timestamp: Date.now(),
|
|
135
|
+
data: operation
|
|
136
|
+
});
|
|
137
|
+
req.onerror = () => reject(req.error);
|
|
138
|
+
req.onsuccess = () => resolve(req.result);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
console.log('[Offline] Operation queued:', operation);
|
|
142
|
+
this.showNotification('Operation queued. Will sync when online.');
|
|
143
|
+
|
|
144
|
+
return true;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error('[DB] Queue failed:', err);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Sync offline operations when back online
|
|
156
|
+
*/
|
|
157
|
+
async syncOperations() {
|
|
158
|
+
if (this.syncInProgress) return;
|
|
159
|
+
if (!this.isDBSupported) return;
|
|
160
|
+
|
|
161
|
+
this.syncInProgress = true;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const tx = this.db.transaction('operationQueue', 'readonly');
|
|
165
|
+
const store = tx.objectStore('operationQueue');
|
|
166
|
+
|
|
167
|
+
const operations = await new Promise((resolve, reject) => {
|
|
168
|
+
const req = store.getAll();
|
|
169
|
+
req.onerror = () => reject(req.error);
|
|
170
|
+
req.onsuccess = () => resolve(req.result);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
console.log('[Offline] Syncing', operations.length, 'queued operations...');
|
|
174
|
+
|
|
175
|
+
if (operations.length === 0) {
|
|
176
|
+
this.syncInProgress = false;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Notify SW to sync
|
|
181
|
+
if (navigator.serviceWorker.controller) {
|
|
182
|
+
navigator.serviceWorker.controller.postMessage({
|
|
183
|
+
type: 'SYNC_OPERATIONS'
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Show sync progress
|
|
188
|
+
this.showSyncProgress(0, operations.length);
|
|
189
|
+
|
|
190
|
+
// Sync each operation
|
|
191
|
+
let syncedCount = 0;
|
|
192
|
+
for (const op of operations) {
|
|
193
|
+
try {
|
|
194
|
+
const response = await fetch('/api/operations', {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: { 'Content-Type': 'application/json' },
|
|
197
|
+
body: JSON.stringify(op.data)
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (response.ok) {
|
|
201
|
+
// Remove from queue
|
|
202
|
+
const txW = this.db.transaction('operationQueue', 'readwrite');
|
|
203
|
+
await new Promise((resolve, reject) => {
|
|
204
|
+
const req = txW.objectStore('operationQueue').delete(op.id);
|
|
205
|
+
req.onerror = () => reject(req.error);
|
|
206
|
+
req.onsuccess = () => resolve();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
syncedCount++;
|
|
210
|
+
this.showSyncProgress(syncedCount, operations.length);
|
|
211
|
+
console.log('[Offline] Synced operation:', op.id);
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error('[Offline] Sync failed for operation:', op.id, err);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
console.log('[Offline] Sync complete:', syncedCount, '/', operations.length);
|
|
219
|
+
this.showNotification(`Synced ${syncedCount} operation(s).`);
|
|
220
|
+
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.error('[Offline] Sync failed:', err);
|
|
223
|
+
this.showNotification('Sync failed. Will retry.');
|
|
224
|
+
} finally {
|
|
225
|
+
this.syncInProgress = false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Handle online event
|
|
231
|
+
*/
|
|
232
|
+
handleOnline() {
|
|
233
|
+
console.log('[Offline] Online detected');
|
|
234
|
+
this.isOnline = true;
|
|
235
|
+
|
|
236
|
+
this.showNotification('Back online! Syncing changes...');
|
|
237
|
+
this.updateOfflineBanner();
|
|
238
|
+
this.syncOperations();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Handle offline event
|
|
243
|
+
*/
|
|
244
|
+
handleOffline() {
|
|
245
|
+
console.log('[Offline] Offline detected');
|
|
246
|
+
this.isOnline = false;
|
|
247
|
+
|
|
248
|
+
this.showNotification('You are offline. Changes will sync when online.');
|
|
249
|
+
this.updateOfflineBanner();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Handle messages from service worker
|
|
254
|
+
*/
|
|
255
|
+
handleSWMessage(data) {
|
|
256
|
+
console.log('[Offline] SW message:', data.type);
|
|
257
|
+
|
|
258
|
+
if (data.type === 'UPDATE_AVAILABLE') {
|
|
259
|
+
this.showUpdatePrompt();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (data.type === 'SYNC_COMPLETE') {
|
|
263
|
+
this.showNotification('All changes synced!');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Show update prompt
|
|
269
|
+
*/
|
|
270
|
+
showUpdatePrompt() {
|
|
271
|
+
this.updateAvailable = true;
|
|
272
|
+
|
|
273
|
+
if (!document.getElementById('update-prompt')) {
|
|
274
|
+
const prompt = document.createElement('div');
|
|
275
|
+
prompt.id = 'update-prompt';
|
|
276
|
+
prompt.innerHTML = `
|
|
277
|
+
<div style="
|
|
278
|
+
position: fixed;
|
|
279
|
+
bottom: 20px;
|
|
280
|
+
right: 20px;
|
|
281
|
+
background: #0284C7;
|
|
282
|
+
color: white;
|
|
283
|
+
padding: 16px 20px;
|
|
284
|
+
border-radius: 8px;
|
|
285
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
286
|
+
z-index: 999999;
|
|
287
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
|
|
288
|
+
max-width: 320px;
|
|
289
|
+
">
|
|
290
|
+
<strong>Update available!</strong>
|
|
291
|
+
<p style="margin: 8px 0 0 0; font-size: 14px;">
|
|
292
|
+
A new version of cycleCAD is ready.
|
|
293
|
+
</p>
|
|
294
|
+
<div style="margin-top: 12px; display: flex; gap: 8px;">
|
|
295
|
+
<button onclick="window.location.reload()" style="
|
|
296
|
+
background: white;
|
|
297
|
+
color: #0284C7;
|
|
298
|
+
border: none;
|
|
299
|
+
padding: 8px 16px;
|
|
300
|
+
border-radius: 4px;
|
|
301
|
+
cursor: pointer;
|
|
302
|
+
font-weight: 600;
|
|
303
|
+
flex: 1;
|
|
304
|
+
">Update Now</button>
|
|
305
|
+
<button onclick="this.closest('#update-prompt').remove()" style="
|
|
306
|
+
background: rgba(255,255,255,0.2);
|
|
307
|
+
color: white;
|
|
308
|
+
border: none;
|
|
309
|
+
padding: 8px 16px;
|
|
310
|
+
border-radius: 4px;
|
|
311
|
+
cursor: pointer;
|
|
312
|
+
flex: 1;
|
|
313
|
+
">Later</button>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
`;
|
|
317
|
+
document.body.appendChild(prompt);
|
|
318
|
+
|
|
319
|
+
// Auto-dismiss after 10 seconds
|
|
320
|
+
setTimeout(() => {
|
|
321
|
+
const el = document.getElementById('update-prompt');
|
|
322
|
+
if (el) el.remove();
|
|
323
|
+
}, 10000);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Show offline banner
|
|
329
|
+
*/
|
|
330
|
+
updateOfflineBanner() {
|
|
331
|
+
let banner = document.getElementById('offline-banner');
|
|
332
|
+
|
|
333
|
+
if (!this.isOnline) {
|
|
334
|
+
if (!banner) {
|
|
335
|
+
banner = document.createElement('div');
|
|
336
|
+
banner.id = 'offline-banner';
|
|
337
|
+
banner.style.cssText = `
|
|
338
|
+
position: fixed;
|
|
339
|
+
top: 0;
|
|
340
|
+
left: 0;
|
|
341
|
+
right: 0;
|
|
342
|
+
background: #EF4444;
|
|
343
|
+
color: white;
|
|
344
|
+
padding: 12px 20px;
|
|
345
|
+
text-align: center;
|
|
346
|
+
font-weight: 500;
|
|
347
|
+
z-index: 999998;
|
|
348
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
|
|
349
|
+
`;
|
|
350
|
+
banner.textContent = 'You are offline. Your changes will sync when you reconnect.';
|
|
351
|
+
document.body.insertBefore(banner, document.body.firstChild);
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
if (banner) banner.remove();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Show notification toast
|
|
360
|
+
*/
|
|
361
|
+
showNotification(message, duration = 4000) {
|
|
362
|
+
const toast = document.createElement('div');
|
|
363
|
+
toast.style.cssText = `
|
|
364
|
+
position: fixed;
|
|
365
|
+
bottom: 20px;
|
|
366
|
+
left: 20px;
|
|
367
|
+
background: #1F2937;
|
|
368
|
+
color: white;
|
|
369
|
+
padding: 16px 20px;
|
|
370
|
+
border-radius: 8px;
|
|
371
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
372
|
+
z-index: 999998;
|
|
373
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
|
|
374
|
+
font-size: 14px;
|
|
375
|
+
max-width: 400px;
|
|
376
|
+
animation: slideIn 0.3s ease-out;
|
|
377
|
+
`;
|
|
378
|
+
toast.textContent = message;
|
|
379
|
+
|
|
380
|
+
document.body.appendChild(toast);
|
|
381
|
+
|
|
382
|
+
setTimeout(() => {
|
|
383
|
+
toast.style.animation = 'slideOut 0.3s ease-in';
|
|
384
|
+
setTimeout(() => toast.remove(), 300);
|
|
385
|
+
}, duration);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Show sync progress
|
|
390
|
+
*/
|
|
391
|
+
showSyncProgress(current, total) {
|
|
392
|
+
let progress = document.getElementById('sync-progress');
|
|
393
|
+
|
|
394
|
+
if (!progress) {
|
|
395
|
+
progress = document.createElement('div');
|
|
396
|
+
progress.id = 'sync-progress';
|
|
397
|
+
progress.style.cssText = `
|
|
398
|
+
position: fixed;
|
|
399
|
+
bottom: 20px;
|
|
400
|
+
left: 20px;
|
|
401
|
+
background: #1F2937;
|
|
402
|
+
color: white;
|
|
403
|
+
padding: 12px 16px;
|
|
404
|
+
border-radius: 8px;
|
|
405
|
+
z-index: 999998;
|
|
406
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
|
|
407
|
+
font-size: 13px;
|
|
408
|
+
min-width: 300px;
|
|
409
|
+
`;
|
|
410
|
+
document.body.appendChild(progress);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const pct = Math.round((current / total) * 100);
|
|
414
|
+
progress.innerHTML = `
|
|
415
|
+
<div>Syncing changes... ${current}/${total}</div>
|
|
416
|
+
<div style="
|
|
417
|
+
background: rgba(255,255,255,0.1);
|
|
418
|
+
height: 4px;
|
|
419
|
+
border-radius: 2px;
|
|
420
|
+
margin-top: 8px;
|
|
421
|
+
overflow: hidden;
|
|
422
|
+
">
|
|
423
|
+
<div style="
|
|
424
|
+
background: #0284C7;
|
|
425
|
+
height: 100%;
|
|
426
|
+
width: ${pct}%;
|
|
427
|
+
transition: width 0.2s ease;
|
|
428
|
+
"></div>
|
|
429
|
+
</div>
|
|
430
|
+
`;
|
|
431
|
+
|
|
432
|
+
if (current >= total) {
|
|
433
|
+
setTimeout(() => progress.remove(), 1000);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Get cache size
|
|
439
|
+
*/
|
|
440
|
+
async getCacheSize() {
|
|
441
|
+
return new Promise((resolve) => {
|
|
442
|
+
if (!this.isSWSupported) {
|
|
443
|
+
resolve(0);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
navigator.serviceWorker.controller?.postMessage(
|
|
448
|
+
{ type: 'GET_CACHE_SIZE' },
|
|
449
|
+
[new MessageChannel().port2]
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
navigator.serviceWorker.addEventListener('message', (event) => {
|
|
453
|
+
if (event.data.size !== undefined) {
|
|
454
|
+
this.cacheSize = event.data.size;
|
|
455
|
+
resolve(event.data.size);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Clear cache
|
|
463
|
+
*/
|
|
464
|
+
async clearCache() {
|
|
465
|
+
return new Promise((resolve) => {
|
|
466
|
+
if (!this.isSWSupported) {
|
|
467
|
+
resolve(false);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const channel = new MessageChannel();
|
|
472
|
+
navigator.serviceWorker.controller?.postMessage(
|
|
473
|
+
{ type: 'CLEAR_CACHE' },
|
|
474
|
+
[channel.port2]
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
channel.port1.onmessage = (event) => {
|
|
478
|
+
if (event.data.success) {
|
|
479
|
+
this.cacheSize = 0;
|
|
480
|
+
this.showNotification('Cache cleared successfully.');
|
|
481
|
+
resolve(true);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Check for updates
|
|
489
|
+
*/
|
|
490
|
+
checkForUpdates() {
|
|
491
|
+
if (!this.isSWSupported) return;
|
|
492
|
+
|
|
493
|
+
// Check every hour
|
|
494
|
+
setInterval(async () => {
|
|
495
|
+
try {
|
|
496
|
+
const registration = await navigator.serviceWorker.getRegistration('/app/');
|
|
497
|
+
if (registration) {
|
|
498
|
+
await registration.update();
|
|
499
|
+
}
|
|
500
|
+
} catch (err) {
|
|
501
|
+
console.error('[Offline] Update check failed:', err);
|
|
502
|
+
}
|
|
503
|
+
}, 60 * 60 * 1000);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Request permissions for notifications and install
|
|
508
|
+
*/
|
|
509
|
+
requestPermissions() {
|
|
510
|
+
// Install prompt
|
|
511
|
+
window.addEventListener('beforeinstallprompt', (event) => {
|
|
512
|
+
event.preventDefault();
|
|
513
|
+
this.installPrompt = event;
|
|
514
|
+
this.showInstallPrompt();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
window.addEventListener('appinstalled', () => {
|
|
518
|
+
console.log('[PWA] App installed successfully');
|
|
519
|
+
this.installPrompt = null;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Show install prompt
|
|
525
|
+
*/
|
|
526
|
+
showInstallPrompt() {
|
|
527
|
+
if (!this.installPrompt || window.matchMedia('(display-mode: standalone)').matches) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!document.getElementById('install-prompt')) {
|
|
532
|
+
const prompt = document.createElement('div');
|
|
533
|
+
prompt.id = 'install-prompt';
|
|
534
|
+
prompt.innerHTML = `
|
|
535
|
+
<div style="
|
|
536
|
+
position: fixed;
|
|
537
|
+
bottom: 20px;
|
|
538
|
+
right: 20px;
|
|
539
|
+
background: white;
|
|
540
|
+
border: 2px solid #0284C7;
|
|
541
|
+
border-radius: 12px;
|
|
542
|
+
padding: 16px 20px;
|
|
543
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
|
|
544
|
+
z-index: 999999;
|
|
545
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
|
|
546
|
+
max-width: 320px;
|
|
547
|
+
">
|
|
548
|
+
<strong style="color: #1F2937;">Add to Home Screen</strong>
|
|
549
|
+
<p style="margin: 8px 0 0 0; font-size: 14px; color: #4B5563;">
|
|
550
|
+
Access cycleCAD directly from your home screen or app drawer.
|
|
551
|
+
</p>
|
|
552
|
+
<div style="margin-top: 12px; display: flex; gap: 8px;">
|
|
553
|
+
<button id="install-btn" style="
|
|
554
|
+
background: #0284C7;
|
|
555
|
+
color: white;
|
|
556
|
+
border: none;
|
|
557
|
+
padding: 8px 16px;
|
|
558
|
+
border-radius: 6px;
|
|
559
|
+
cursor: pointer;
|
|
560
|
+
font-weight: 600;
|
|
561
|
+
flex: 1;
|
|
562
|
+
">Install</button>
|
|
563
|
+
<button onclick="this.closest('#install-prompt').remove()" style="
|
|
564
|
+
background: #F3F4F6;
|
|
565
|
+
color: #1F2937;
|
|
566
|
+
border: none;
|
|
567
|
+
padding: 8px 16px;
|
|
568
|
+
border-radius: 6px;
|
|
569
|
+
cursor: pointer;
|
|
570
|
+
flex: 1;
|
|
571
|
+
">Not now</button>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
`;
|
|
575
|
+
document.body.appendChild(prompt);
|
|
576
|
+
|
|
577
|
+
document.getElementById('install-btn').addEventListener('click', () => {
|
|
578
|
+
this.installPrompt.prompt();
|
|
579
|
+
this.installPrompt.userChoice.then((choice) => {
|
|
580
|
+
if (choice.outcome === 'accepted') {
|
|
581
|
+
console.log('[PWA] Install accepted');
|
|
582
|
+
}
|
|
583
|
+
document.getElementById('install-prompt')?.remove();
|
|
584
|
+
this.installPrompt = null;
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Auto-dismiss after 15 seconds
|
|
589
|
+
setTimeout(() => {
|
|
590
|
+
const el = document.getElementById('install-prompt');
|
|
591
|
+
if (el) el.remove();
|
|
592
|
+
}, 15000);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Setup UI controls
|
|
598
|
+
*/
|
|
599
|
+
setupUI() {
|
|
600
|
+
// Add offline manager UI to settings panel if it exists
|
|
601
|
+
const settingsPanel = document.getElementById('settings-panel');
|
|
602
|
+
if (settingsPanel) {
|
|
603
|
+
const offlineSection = document.createElement('div');
|
|
604
|
+
offlineSection.id = 'offline-section';
|
|
605
|
+
offlineSection.innerHTML = `
|
|
606
|
+
<div style="padding: 12px 0; border-top: 1px solid #E5E7EB;">
|
|
607
|
+
<h3 style="margin: 0 0 12px 0; font-size: 14px; font-weight: 600; color: #1F2937;">
|
|
608
|
+
Offline & Cache
|
|
609
|
+
</h3>
|
|
610
|
+
<button id="cache-status-btn" style="
|
|
611
|
+
width: 100%;
|
|
612
|
+
padding: 10px;
|
|
613
|
+
margin-bottom: 8px;
|
|
614
|
+
background: #F3F4F6;
|
|
615
|
+
border: 1px solid #D1D5DB;
|
|
616
|
+
border-radius: 6px;
|
|
617
|
+
cursor: pointer;
|
|
618
|
+
font-size: 13px;
|
|
619
|
+
text-align: left;
|
|
620
|
+
">
|
|
621
|
+
<div>Cache: Calculating...</div>
|
|
622
|
+
</button>
|
|
623
|
+
<button id="clear-cache-btn" style="
|
|
624
|
+
width: 100%;
|
|
625
|
+
padding: 10px;
|
|
626
|
+
background: #FEE2E2;
|
|
627
|
+
border: 1px solid #FCA5A5;
|
|
628
|
+
border-radius: 6px;
|
|
629
|
+
cursor: pointer;
|
|
630
|
+
font-weight: 500;
|
|
631
|
+
color: #DC2626;
|
|
632
|
+
font-size: 13px;
|
|
633
|
+
">
|
|
634
|
+
Clear Cache
|
|
635
|
+
</button>
|
|
636
|
+
</div>
|
|
637
|
+
`;
|
|
638
|
+
settingsPanel.appendChild(offlineSection);
|
|
639
|
+
|
|
640
|
+
// Cache size display
|
|
641
|
+
this.getCacheSize().then((size) => {
|
|
642
|
+
const btn = document.getElementById('cache-status-btn');
|
|
643
|
+
if (btn) {
|
|
644
|
+
const sizeStr = this.formatBytes(size);
|
|
645
|
+
btn.textContent = `Cache: ${sizeStr}`;
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Clear cache handler
|
|
650
|
+
document.getElementById('clear-cache-btn')?.addEventListener('click', async () => {
|
|
651
|
+
if (confirm('Clear all cached files? You can still work offline with cached projects.')) {
|
|
652
|
+
await this.clearCache();
|
|
653
|
+
const btn = document.getElementById('cache-status-btn');
|
|
654
|
+
if (btn) btn.textContent = 'Cache: 0 B';
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Format bytes to human-readable
|
|
662
|
+
*/
|
|
663
|
+
formatBytes(bytes) {
|
|
664
|
+
if (bytes === 0) return '0 B';
|
|
665
|
+
const k = 1024;
|
|
666
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
667
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
668
|
+
return Math.round(bytes / Math.pow(k, i) * 10) / 10 + ' ' + sizes[i];
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Initialize on page load
|
|
673
|
+
window.offlineManager = new OfflineManager();
|
|
674
|
+
|
|
675
|
+
// Add styles for animations
|
|
676
|
+
const style = document.createElement('style');
|
|
677
|
+
style.textContent = `
|
|
678
|
+
@keyframes slideIn {
|
|
679
|
+
from {
|
|
680
|
+
opacity: 0;
|
|
681
|
+
transform: translateX(-20px);
|
|
682
|
+
}
|
|
683
|
+
to {
|
|
684
|
+
opacity: 1;
|
|
685
|
+
transform: translateX(0);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
@keyframes slideOut {
|
|
690
|
+
from {
|
|
691
|
+
opacity: 1;
|
|
692
|
+
transform: translateX(0);
|
|
693
|
+
}
|
|
694
|
+
to {
|
|
695
|
+
opacity: 0;
|
|
696
|
+
transform: translateX(-20px);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
`;
|
|
700
|
+
document.head.appendChild(style);
|
|
701
|
+
|
|
702
|
+
// Export for testing
|
|
703
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
704
|
+
module.exports = OfflineManager;
|
|
705
|
+
}
|