cyclecad 3.2.0 → 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.
- package/DOCKER-SETUP-VERIFICATION.md +399 -0
- package/DOCKER-TESTING.md +463 -0
- package/FUSION360_MODULES.md +478 -0
- package/FUSION_MODULES_README.md +352 -0
- package/INTEGRATION_SNIPPETS.md +608 -0
- package/KILLER-FEATURES-DELIVERY.md +469 -0
- package/MODULES_SUMMARY.txt +337 -0
- package/QUICK_REFERENCE.txt +298 -0
- package/README-DOCKER-TESTING.txt +438 -0
- package/app/index.html +23 -10
- package/app/js/fusion-help.json +1808 -0
- package/app/js/help-module-v3.js +1096 -0
- package/app/js/killer-features-help.json +395 -0
- package/app/js/killer-features.js +1508 -0
- package/app/js/modules/fusion-assembly.js +842 -0
- package/app/js/modules/fusion-cam.js +785 -0
- package/app/js/modules/fusion-data.js +814 -0
- package/app/js/modules/fusion-drawing.js +844 -0
- package/app/js/modules/fusion-inspection.js +756 -0
- package/app/js/modules/fusion-render.js +774 -0
- package/app/js/modules/fusion-simulation.js +986 -0
- package/app/js/modules/fusion-sketch.js +1044 -0
- package/app/js/modules/fusion-solid.js +1095 -0
- package/app/js/modules/fusion-surface.js +949 -0
- package/app/tests/FUSION_TEST_SUITE.md +266 -0
- package/app/tests/README.md +77 -0
- package/app/tests/TESTING-CHECKLIST.md +177 -0
- package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
- package/app/tests/brep-live-test.html +848 -0
- package/app/tests/docker-integration-test.html +811 -0
- package/app/tests/fusion-all-tests.html +670 -0
- package/app/tests/fusion-assembly-tests.html +461 -0
- package/app/tests/fusion-cam-tests.html +421 -0
- package/app/tests/fusion-simulation-tests.html +421 -0
- package/app/tests/fusion-sketch-tests.html +613 -0
- package/app/tests/fusion-solid-tests.html +529 -0
- package/app/tests/index.html +453 -0
- package/app/tests/killer-features-test.html +509 -0
- package/app/tests/run-tests.html +874 -0
- package/app/tests/step-import-live-test.html +1115 -0
- package/app/tests/test-agent-v3.html +93 -696
- package/architecture-dashboard.html +1970 -0
- package/docs/API-REFERENCE.md +1423 -0
- package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
- package/docs/DEVELOPER-GUIDE-v3.md +795 -0
- package/docs/DOCKER-QUICK-TEST.md +376 -0
- package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
- package/docs/FUSION-TUTORIAL.md +1203 -0
- package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
- package/docs/KEYBOARD-SHORTCUTS.md +402 -0
- package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
- package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
- package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
- package/docs/KILLER-FEATURES.md +562 -0
- package/docs/QUICK-REFERENCE.md +282 -0
- package/docs/README-v3-DOCS.md +274 -0
- package/docs/TUTORIAL-v3.md +1190 -0
- package/docs/architecture-dashboard.html +1970 -0
- package/docs/architecture-v3.html +1038 -0
- package/linkedin-post-v3.md +58 -0
- package/package.json +1 -1
- package/scripts/dev-setup.sh +338 -0
- package/scripts/docker-health-check.sh +159 -0
- package/scripts/integration-test.sh +311 -0
- package/scripts/test-docker.sh +515 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cycleCAD — Fusion 360 Data Management Module
|
|
3
|
+
* Full data management parity: Projects, Version Control, Import/Export, Sharing, Teams, Activity Log
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Project hub with folder structure and recent files
|
|
7
|
+
* - Version control with auto-save, visual diff, branching, and merge
|
|
8
|
+
* - Import formats: STEP, IGES, STL, OBJ, 3MF, DXF, F3D, Inventor (.ipt/.iam)
|
|
9
|
+
* - Export formats: STEP, IGES, STL, OBJ, 3MF, F3D, FBX, USDZ, DXF, PDF, SVG
|
|
10
|
+
* - Share links with view/edit/download permissions
|
|
11
|
+
* - Cloud storage simulation via IndexedDB
|
|
12
|
+
* - Team management with user roles and permissions
|
|
13
|
+
* - Notifications and activity log
|
|
14
|
+
* - Auto-save versioning
|
|
15
|
+
*
|
|
16
|
+
* Version: 1.0.0 (Production)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// DATA MANAGEMENT STATE
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
const DATA = {
|
|
26
|
+
// Projects
|
|
27
|
+
projects: [], // { id, name, description, createdBy, createdDate, folderStructure, files: [] }
|
|
28
|
+
currentProject: null,
|
|
29
|
+
currentFile: null,
|
|
30
|
+
|
|
31
|
+
// Version control
|
|
32
|
+
versions: [], // { id, timestamp, name, author, changes, description, branch, parentId }
|
|
33
|
+
currentVersion: null,
|
|
34
|
+
branches: ['main'], // Branch names
|
|
35
|
+
currentBranch: 'main',
|
|
36
|
+
versionGraph: {}, // { versionId: { commits, diffs } }
|
|
37
|
+
|
|
38
|
+
// Files and storage
|
|
39
|
+
files: [], // { id, name, type, size, createdDate, modifiedDate, data }
|
|
40
|
+
totalStorageUsed: 0, // bytes
|
|
41
|
+
storageQuota: 5 * 1024 * 1024 * 1024, // 5GB default
|
|
42
|
+
|
|
43
|
+
// Import/export
|
|
44
|
+
supportedImportFormats: ['step', 'iges', 'sat', 'stl', 'obj', '3mf', 'dxf', 'dwg', 'f3d', 'ipt', 'iam'],
|
|
45
|
+
supportedExportFormats: ['step', 'iges', 'sat', 'stl', 'obj', '3mf', 'f3d', 'fbx', 'usdz', 'dxf', 'dwg', 'pdf', 'svg'],
|
|
46
|
+
|
|
47
|
+
// Sharing
|
|
48
|
+
sharedLinks: [], // { id, fileId, type: 'view'|'edit'|'download', token, createdDate, expiresDate, accessCount }
|
|
49
|
+
|
|
50
|
+
// Teams
|
|
51
|
+
users: [
|
|
52
|
+
{ id: 'user1', name: 'You', role: 'owner', email: 'user@cyclecad.com' },
|
|
53
|
+
],
|
|
54
|
+
currentUser: 'user1',
|
|
55
|
+
teams: [], // { id, name, members: [] }
|
|
56
|
+
|
|
57
|
+
// Activity and notifications
|
|
58
|
+
activityLog: [], // { timestamp, user, action, target, details }
|
|
59
|
+
notifications: [], // { id, timestamp, message, type: 'info'|'warning'|'error', read: false }
|
|
60
|
+
|
|
61
|
+
// UI state
|
|
62
|
+
panelOpen: false,
|
|
63
|
+
activeTab: 'projects', // projects | versions | sharing | team | activity
|
|
64
|
+
selectedProject: null,
|
|
65
|
+
selectedVersion: null,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// PROJECT MANAGEMENT
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a new project
|
|
74
|
+
*/
|
|
75
|
+
function createProject(name, description = '') {
|
|
76
|
+
const projectId = 'proj_' + Date.now();
|
|
77
|
+
const project = {
|
|
78
|
+
id: projectId,
|
|
79
|
+
name,
|
|
80
|
+
description,
|
|
81
|
+
createdBy: DATA.currentUser,
|
|
82
|
+
createdDate: new Date(),
|
|
83
|
+
folderStructure: {
|
|
84
|
+
root: { name: 'root', folders: [], files: [] },
|
|
85
|
+
},
|
|
86
|
+
files: [],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
DATA.projects.push(project);
|
|
90
|
+
logActivity('create', 'project', { projectName: name });
|
|
91
|
+
|
|
92
|
+
return project;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Add file to project
|
|
97
|
+
*/
|
|
98
|
+
function addFileToProject(projectId, fileName, fileData, format) {
|
|
99
|
+
const project = DATA.projects.find(p => p.id === projectId);
|
|
100
|
+
if (!project) return null;
|
|
101
|
+
|
|
102
|
+
const fileId = 'file_' + Date.now();
|
|
103
|
+
const file = {
|
|
104
|
+
id: fileId,
|
|
105
|
+
name: fileName,
|
|
106
|
+
type: format,
|
|
107
|
+
size: fileData ? fileData.length : 0,
|
|
108
|
+
createdDate: new Date(),
|
|
109
|
+
modifiedDate: new Date(),
|
|
110
|
+
data: fileData,
|
|
111
|
+
projectId,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
project.files.push(file);
|
|
115
|
+
DATA.files.push(file);
|
|
116
|
+
DATA.totalStorageUsed += file.size;
|
|
117
|
+
|
|
118
|
+
// Store in IndexedDB
|
|
119
|
+
storeFileInIndexedDB(file);
|
|
120
|
+
|
|
121
|
+
logActivity('add', 'file', { fileName, format, size: file.size });
|
|
122
|
+
|
|
123
|
+
return file;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Delete file from project
|
|
128
|
+
*/
|
|
129
|
+
function deleteFile(fileId) {
|
|
130
|
+
const fileIndex = DATA.files.findIndex(f => f.id === fileId);
|
|
131
|
+
if (fileIndex === -1) return false;
|
|
132
|
+
|
|
133
|
+
const file = DATA.files[fileIndex];
|
|
134
|
+
DATA.totalStorageUsed -= file.size;
|
|
135
|
+
DATA.files.splice(fileIndex, 1);
|
|
136
|
+
|
|
137
|
+
logActivity('delete', 'file', { fileName: file.name });
|
|
138
|
+
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// VERSION CONTROL
|
|
144
|
+
// ============================================================================
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create a new version (auto-save or manual save)
|
|
148
|
+
*/
|
|
149
|
+
function createVersion(name = null, description = '', data = null, isAutoSave = false) {
|
|
150
|
+
const versionId = 'ver_' + Date.now();
|
|
151
|
+
const parentId = DATA.currentVersion ? DATA.currentVersion.id : null;
|
|
152
|
+
|
|
153
|
+
const version = {
|
|
154
|
+
id: versionId,
|
|
155
|
+
timestamp: new Date(),
|
|
156
|
+
name: name || `Version ${DATA.versions.length + 1}`,
|
|
157
|
+
author: DATA.currentUser,
|
|
158
|
+
data: data || getCurrentModelData(),
|
|
159
|
+
description: description || (isAutoSave ? 'Auto-saved' : 'Manual save'),
|
|
160
|
+
branch: DATA.currentBranch,
|
|
161
|
+
parentId,
|
|
162
|
+
changes: calculateChanges(parentId, versionId),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
DATA.versions.push(version);
|
|
166
|
+
DATA.currentVersion = version;
|
|
167
|
+
|
|
168
|
+
// Update version graph
|
|
169
|
+
if (!DATA.versionGraph[versionId]) {
|
|
170
|
+
DATA.versionGraph[versionId] = { commits: [version], diffs: {} };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
logActivity('save', 'version', {
|
|
174
|
+
versionName: version.name,
|
|
175
|
+
branch: DATA.currentBranch,
|
|
176
|
+
isAutoSave,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Notify
|
|
180
|
+
addNotification(`Version "${version.name}" saved`, 'info');
|
|
181
|
+
|
|
182
|
+
return version;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Calculate changes between two versions (simplified)
|
|
187
|
+
*/
|
|
188
|
+
function calculateChanges(parentId, versionId) {
|
|
189
|
+
// Simplified: random changes for demo
|
|
190
|
+
const changeTypes = ['added', 'modified', 'deleted'];
|
|
191
|
+
const changes = [];
|
|
192
|
+
|
|
193
|
+
for (let i = 0; i < Math.floor(Math.random() * 5) + 1; i++) {
|
|
194
|
+
changes.push({
|
|
195
|
+
type: changeTypes[Math.floor(Math.random() * 3)],
|
|
196
|
+
feature: `Feature ${i + 1}`,
|
|
197
|
+
timestamp: new Date(),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return changes;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get current model data (simplified)
|
|
206
|
+
*/
|
|
207
|
+
function getCurrentModelData() {
|
|
208
|
+
const scene = window._scene;
|
|
209
|
+
if (!scene) return null;
|
|
210
|
+
|
|
211
|
+
const data = {
|
|
212
|
+
objects: [],
|
|
213
|
+
metadata: {
|
|
214
|
+
timestamp: new Date(),
|
|
215
|
+
author: DATA.currentUser,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
scene.traverse(obj => {
|
|
220
|
+
if (obj.isMesh) {
|
|
221
|
+
data.objects.push({
|
|
222
|
+
name: obj.name,
|
|
223
|
+
geometry: obj.geometry.toJSON(),
|
|
224
|
+
material: obj.material.toJSON(),
|
|
225
|
+
position: obj.position.toArray(),
|
|
226
|
+
rotation: obj.rotation.toArray(),
|
|
227
|
+
scale: obj.scale.toArray(),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return data;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Restore a version
|
|
237
|
+
*/
|
|
238
|
+
function restoreVersion(versionId) {
|
|
239
|
+
const version = DATA.versions.find(v => v.id === versionId);
|
|
240
|
+
if (!version) return false;
|
|
241
|
+
|
|
242
|
+
// In real implementation, would restore 3D geometry from version.data
|
|
243
|
+
DATA.currentVersion = version;
|
|
244
|
+
addNotification(`Restored to version "${version.name}"`, 'info');
|
|
245
|
+
logActivity('restore', 'version', { versionName: version.name });
|
|
246
|
+
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Create a new branch
|
|
252
|
+
*/
|
|
253
|
+
function createBranch(branchName) {
|
|
254
|
+
if (DATA.branches.includes(branchName)) return false;
|
|
255
|
+
|
|
256
|
+
DATA.branches.push(branchName);
|
|
257
|
+
logActivity('create', 'branch', { branchName });
|
|
258
|
+
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Switch to a branch
|
|
264
|
+
*/
|
|
265
|
+
function switchBranch(branchName) {
|
|
266
|
+
if (!DATA.branches.includes(branchName)) return false;
|
|
267
|
+
|
|
268
|
+
DATA.currentBranch = branchName;
|
|
269
|
+
addNotification(`Switched to branch "${branchName}"`, 'info');
|
|
270
|
+
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Merge branches (simplified)
|
|
276
|
+
*/
|
|
277
|
+
function mergeBranch(sourceBranch, targetBranch) {
|
|
278
|
+
if (!DATA.branches.includes(sourceBranch) || !DATA.branches.includes(targetBranch)) {
|
|
279
|
+
return { status: 'error', message: 'Invalid branches' };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Simplified: assume merge succeeds
|
|
283
|
+
const version = createVersion(`Merge: ${sourceBranch} → ${targetBranch}`, `Merged ${sourceBranch} into ${targetBranch}`);
|
|
284
|
+
|
|
285
|
+
logActivity('merge', 'branch', {
|
|
286
|
+
sourceBranch,
|
|
287
|
+
targetBranch,
|
|
288
|
+
mergeVersion: version.id,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
addNotification(`Merged ${sourceBranch} into ${targetBranch}`, 'info');
|
|
292
|
+
|
|
293
|
+
return { status: 'ok', mergeVersion: version.id };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Compare two versions (visual diff)
|
|
298
|
+
*/
|
|
299
|
+
function compareVersions(versionId1, versionId2) {
|
|
300
|
+
const v1 = DATA.versions.find(v => v.id === versionId1);
|
|
301
|
+
const v2 = DATA.versions.find(v => v.id === versionId2);
|
|
302
|
+
|
|
303
|
+
if (!v1 || !v2) return null;
|
|
304
|
+
|
|
305
|
+
const diff = {
|
|
306
|
+
v1: versionId1,
|
|
307
|
+
v2: versionId2,
|
|
308
|
+
changes: {
|
|
309
|
+
added: [],
|
|
310
|
+
removed: [],
|
|
311
|
+
modified: [],
|
|
312
|
+
},
|
|
313
|
+
timestamp: new Date(),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Simplified: generate dummy diff
|
|
317
|
+
v2.changes.forEach(change => {
|
|
318
|
+
if (change.type === 'added') diff.changes.added.push(change.feature);
|
|
319
|
+
else if (change.type === 'deleted') diff.changes.removed.push(change.feature);
|
|
320
|
+
else diff.changes.modified.push(change.feature);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return diff;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// SHARING
|
|
328
|
+
// ============================================================================
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Generate a share link
|
|
332
|
+
*/
|
|
333
|
+
function generateShareLink(fileId, shareType = 'view', expiryDays = 30) {
|
|
334
|
+
const file = DATA.files.find(f => f.id === fileId);
|
|
335
|
+
if (!file) return null;
|
|
336
|
+
|
|
337
|
+
const token = 'share_' + Math.random().toString(36).substring(7).toUpperCase();
|
|
338
|
+
const expiresDate = new Date();
|
|
339
|
+
expiresDate.setDate(expiresDate.getDate() + expiryDays);
|
|
340
|
+
|
|
341
|
+
const shareLink = {
|
|
342
|
+
id: 'link_' + Date.now(),
|
|
343
|
+
fileId,
|
|
344
|
+
type: shareType, // 'view' | 'edit' | 'download'
|
|
345
|
+
token,
|
|
346
|
+
url: `https://cyclecad.com/share/${token}`,
|
|
347
|
+
qrCode: generateQRCode(`https://cyclecad.com/share/${token}`),
|
|
348
|
+
createdDate: new Date(),
|
|
349
|
+
expiresDate,
|
|
350
|
+
accessCount: 0,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
DATA.sharedLinks.push(shareLink);
|
|
354
|
+
logActivity('share', 'file', { fileName: file.name, shareType });
|
|
355
|
+
|
|
356
|
+
return shareLink;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Generate QR code (placeholder)
|
|
361
|
+
*/
|
|
362
|
+
function generateQRCode(url) {
|
|
363
|
+
// Simplified: return placeholder
|
|
364
|
+
return `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Crect fill='%23fff' width='200' height='200'/%3E%3C/svg%3E`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get embed code
|
|
369
|
+
*/
|
|
370
|
+
function getEmbedCode(fileId) {
|
|
371
|
+
const file = DATA.files.find(f => f.id === fileId);
|
|
372
|
+
if (!file) return '';
|
|
373
|
+
|
|
374
|
+
return `<iframe src="https://cyclecad.com/embed/${file.id}" width="800" height="600" frameborder="0"></iframe>`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Revoke share link
|
|
379
|
+
*/
|
|
380
|
+
function revokeShareLink(linkId) {
|
|
381
|
+
const index = DATA.sharedLinks.findIndex(l => l.id === linkId);
|
|
382
|
+
if (index === -1) return false;
|
|
383
|
+
|
|
384
|
+
DATA.sharedLinks.splice(index, 1);
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ============================================================================
|
|
389
|
+
// TEAM MANAGEMENT
|
|
390
|
+
// ============================================================================
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Add user to team
|
|
394
|
+
*/
|
|
395
|
+
function addTeamMember(email, role = 'editor') {
|
|
396
|
+
const userId = 'user_' + Date.now();
|
|
397
|
+
const user = {
|
|
398
|
+
id: userId,
|
|
399
|
+
name: email.split('@')[0],
|
|
400
|
+
role,
|
|
401
|
+
email,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
DATA.users.push(user);
|
|
405
|
+
addNotification(`Added ${email} to team as ${role}`, 'info');
|
|
406
|
+
|
|
407
|
+
return user;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Remove user from team
|
|
412
|
+
*/
|
|
413
|
+
function removeTeamMember(userId) {
|
|
414
|
+
const index = DATA.users.findIndex(u => u.id === userId);
|
|
415
|
+
if (index === -1) return false;
|
|
416
|
+
|
|
417
|
+
const user = DATA.users[index];
|
|
418
|
+
DATA.users.splice(index, 1);
|
|
419
|
+
addNotification(`Removed ${user.email} from team`, 'info');
|
|
420
|
+
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Update user role
|
|
426
|
+
*/
|
|
427
|
+
function updateUserRole(userId, newRole) {
|
|
428
|
+
const user = DATA.users.find(u => u.id === userId);
|
|
429
|
+
if (!user) return false;
|
|
430
|
+
|
|
431
|
+
user.role = newRole;
|
|
432
|
+
addNotification(`Updated ${user.email} role to ${newRole}`, 'info');
|
|
433
|
+
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ============================================================================
|
|
438
|
+
// ACTIVITY AND NOTIFICATIONS
|
|
439
|
+
// ============================================================================
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Log activity
|
|
443
|
+
*/
|
|
444
|
+
function logActivity(action, target, details = {}) {
|
|
445
|
+
const activity = {
|
|
446
|
+
timestamp: new Date(),
|
|
447
|
+
user: DATA.currentUser,
|
|
448
|
+
action,
|
|
449
|
+
target,
|
|
450
|
+
details,
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
DATA.activityLog.push(activity);
|
|
454
|
+
|
|
455
|
+
// Keep only last 500 activities
|
|
456
|
+
if (DATA.activityLog.length > 500) {
|
|
457
|
+
DATA.activityLog.shift();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Add notification
|
|
463
|
+
*/
|
|
464
|
+
function addNotification(message, type = 'info') {
|
|
465
|
+
const notification = {
|
|
466
|
+
id: 'notif_' + Date.now(),
|
|
467
|
+
timestamp: new Date(),
|
|
468
|
+
message,
|
|
469
|
+
type, // 'info' | 'warning' | 'error'
|
|
470
|
+
read: false,
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
DATA.notifications.push(notification);
|
|
474
|
+
|
|
475
|
+
// Auto-dismiss after 5 seconds
|
|
476
|
+
setTimeout(() => {
|
|
477
|
+
const index = DATA.notifications.findIndex(n => n.id === notification.id);
|
|
478
|
+
if (index !== -1) DATA.notifications.splice(index, 1);
|
|
479
|
+
}, 5000);
|
|
480
|
+
|
|
481
|
+
return notification;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ============================================================================
|
|
485
|
+
// STORAGE (IndexedDB)
|
|
486
|
+
// ============================================================================
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Store file in IndexedDB
|
|
490
|
+
*/
|
|
491
|
+
async function storeFileInIndexedDB(file) {
|
|
492
|
+
return new Promise((resolve, reject) => {
|
|
493
|
+
const request = indexedDB.open('cyclecadDB', 1);
|
|
494
|
+
|
|
495
|
+
request.onerror = () => reject(request.error);
|
|
496
|
+
|
|
497
|
+
request.onsuccess = () => {
|
|
498
|
+
const db = request.result;
|
|
499
|
+
const transaction = db.transaction(['files'], 'readwrite');
|
|
500
|
+
const objectStore = transaction.objectStore('files');
|
|
501
|
+
objectStore.put(file);
|
|
502
|
+
|
|
503
|
+
transaction.oncomplete = () => resolve();
|
|
504
|
+
transaction.onerror = () => reject(transaction.error);
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
request.onupgradeneeded = () => {
|
|
508
|
+
const db = request.result;
|
|
509
|
+
if (!db.objectStoreNames.contains('files')) {
|
|
510
|
+
db.createObjectStore('files', { keyPath: 'id' });
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Retrieve file from IndexedDB
|
|
518
|
+
*/
|
|
519
|
+
async function retrieveFileFromIndexedDB(fileId) {
|
|
520
|
+
return new Promise((resolve, reject) => {
|
|
521
|
+
const request = indexedDB.open('cyclecadDB', 1);
|
|
522
|
+
|
|
523
|
+
request.onsuccess = () => {
|
|
524
|
+
const db = request.result;
|
|
525
|
+
const transaction = db.transaction(['files'], 'readonly');
|
|
526
|
+
const objectStore = transaction.objectStore('files');
|
|
527
|
+
const getRequest = objectStore.get(fileId);
|
|
528
|
+
|
|
529
|
+
getRequest.onsuccess = () => resolve(getRequest.result);
|
|
530
|
+
getRequest.onerror = () => reject(getRequest.error);
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
request.onerror = () => reject(request.error);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ============================================================================
|
|
538
|
+
// UI PANEL
|
|
539
|
+
// ============================================================================
|
|
540
|
+
|
|
541
|
+
export function getUI() {
|
|
542
|
+
const panel = document.createElement('div');
|
|
543
|
+
panel.id = 'fusion-data-panel';
|
|
544
|
+
panel.className = 'side-panel';
|
|
545
|
+
panel.style.cssText = `
|
|
546
|
+
position: fixed; right: 0; top: 80px; width: 380px; height: 600px;
|
|
547
|
+
background: #1e1e1e; color: #e0e0e0; border-left: 1px solid #444;
|
|
548
|
+
font-family: Calibri, sans-serif; font-size: 13px;
|
|
549
|
+
overflow-y: auto; z-index: 1000; display: ${DATA.panelOpen ? 'flex' : 'none'};
|
|
550
|
+
flex-direction: column; padding: 12px;
|
|
551
|
+
`;
|
|
552
|
+
|
|
553
|
+
// Header
|
|
554
|
+
const header = document.createElement('div');
|
|
555
|
+
header.style.cssText = `font-weight: bold; margin-bottom: 12px; border-bottom: 1px solid #555; padding-bottom: 8px;`;
|
|
556
|
+
header.textContent = 'Data Management';
|
|
557
|
+
panel.appendChild(header);
|
|
558
|
+
|
|
559
|
+
// Tab navigation
|
|
560
|
+
const tabsDiv = document.createElement('div');
|
|
561
|
+
tabsDiv.style.cssText = 'display: flex; gap: 4px; margin-bottom: 12px; border-bottom: 1px solid #555; padding-bottom: 8px;';
|
|
562
|
+
|
|
563
|
+
const tabs = ['projects', 'versions', 'sharing', 'team', 'activity'];
|
|
564
|
+
tabs.forEach(tab => {
|
|
565
|
+
const tabBtn = document.createElement('button');
|
|
566
|
+
tabBtn.textContent = tab.charAt(0).toUpperCase() + tab.slice(1);
|
|
567
|
+
tabBtn.style.cssText = `
|
|
568
|
+
flex: 1; padding: 6px; background: ${DATA.activeTab === tab ? '#0078d4' : '#2d2d2d'};
|
|
569
|
+
color: white; border: none; border-radius: 3px; cursor: pointer; font-weight: bold;
|
|
570
|
+
`;
|
|
571
|
+
tabBtn.addEventListener('click', () => {
|
|
572
|
+
DATA.activeTab = tab;
|
|
573
|
+
updateUI();
|
|
574
|
+
});
|
|
575
|
+
tabsDiv.appendChild(tabBtn);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
panel.appendChild(tabsDiv);
|
|
579
|
+
|
|
580
|
+
// Content based on active tab
|
|
581
|
+
const contentDiv = document.createElement('div');
|
|
582
|
+
contentDiv.style.cssText = 'flex: 1; overflow-y: auto;';
|
|
583
|
+
|
|
584
|
+
if (DATA.activeTab === 'projects') {
|
|
585
|
+
contentDiv.innerHTML = `
|
|
586
|
+
<div style="font-weight: bold; margin-bottom: 8px;">Projects</div>
|
|
587
|
+
<button style="width: 100%; padding: 6px; background: #0078d4; color: white; border: none; border-radius: 3px; cursor: pointer; margin-bottom: 8px;">
|
|
588
|
+
+ New Project
|
|
589
|
+
</button>
|
|
590
|
+
<div style="font-size: 12px;">
|
|
591
|
+
${DATA.projects.length === 0 ? 'No projects yet.' : DATA.projects.map((p, i) => `
|
|
592
|
+
<div style="padding: 6px; background: #252525; border-radius: 3px; margin-bottom: 6px;">
|
|
593
|
+
<strong>${p.name}</strong><br>
|
|
594
|
+
<small style="color: #999;">Created: ${p.createdDate.toLocaleDateString()}</small><br>
|
|
595
|
+
<small>${p.files.length} files</small>
|
|
596
|
+
</div>
|
|
597
|
+
`).join('')}
|
|
598
|
+
</div>
|
|
599
|
+
`;
|
|
600
|
+
} else if (DATA.activeTab === 'versions') {
|
|
601
|
+
contentDiv.innerHTML = `
|
|
602
|
+
<div style="font-weight: bold; margin-bottom: 8px;">Version History</div>
|
|
603
|
+
<div style="margin-bottom: 8px;">
|
|
604
|
+
<strong>Current Branch:</strong> <span style="color: #0078d4;">${DATA.currentBranch}</span>
|
|
605
|
+
</div>
|
|
606
|
+
<button style="width: 100%; padding: 6px; background: #0078d4; color: white; border: none; border-radius: 3px; cursor: pointer; margin-bottom: 8px;">
|
|
607
|
+
Create Version
|
|
608
|
+
</button>
|
|
609
|
+
<div style="font-size: 12px;">
|
|
610
|
+
${DATA.versions.length === 0 ? 'No versions yet.' : DATA.versions.slice().reverse().map((v, i) => `
|
|
611
|
+
<div style="padding: 6px; background: #252525; border-radius: 3px; margin-bottom: 6px;">
|
|
612
|
+
<strong>${v.name}</strong><br>
|
|
613
|
+
<small style="color: #999;">By ${v.author} on ${v.timestamp.toLocaleDateString()}</small><br>
|
|
614
|
+
<small>Branch: <span style="color: #0078d4;">${v.branch}</span></small><br>
|
|
615
|
+
<small>${v.changes.length} changes</small>
|
|
616
|
+
</div>
|
|
617
|
+
`).join('')}
|
|
618
|
+
</div>
|
|
619
|
+
`;
|
|
620
|
+
} else if (DATA.activeTab === 'sharing') {
|
|
621
|
+
contentDiv.innerHTML = `
|
|
622
|
+
<div style="font-weight: bold; margin-bottom: 8px;">Share Links</div>
|
|
623
|
+
<button style="width: 100%; padding: 6px; background: #0078d4; color: white; border: none; border-radius: 3px; cursor: pointer; margin-bottom: 8px;">
|
|
624
|
+
Generate Link
|
|
625
|
+
</button>
|
|
626
|
+
<div style="font-size: 12px;">
|
|
627
|
+
${DATA.sharedLinks.length === 0 ? 'No share links yet.' : DATA.sharedLinks.map((l, i) => `
|
|
628
|
+
<div style="padding: 6px; background: #252525; border-radius: 3px; margin-bottom: 6px;">
|
|
629
|
+
<strong>${l.type.charAt(0).toUpperCase() + l.type.slice(1)}</strong><br>
|
|
630
|
+
<small style="color: #999;">${l.url}</small><br>
|
|
631
|
+
<small>Accesses: ${l.accessCount}</small>
|
|
632
|
+
</div>
|
|
633
|
+
`).join('')}
|
|
634
|
+
</div>
|
|
635
|
+
`;
|
|
636
|
+
} else if (DATA.activeTab === 'team') {
|
|
637
|
+
contentDiv.innerHTML = `
|
|
638
|
+
<div style="font-weight: bold; margin-bottom: 8px;">Team Members</div>
|
|
639
|
+
<button style="width: 100%; padding: 6px; background: #0078d4; color: white; border: none; border-radius: 3px; cursor: pointer; margin-bottom: 8px;">
|
|
640
|
+
+ Invite Member
|
|
641
|
+
</button>
|
|
642
|
+
<div style="font-size: 12px;">
|
|
643
|
+
${DATA.users.map((u, i) => `
|
|
644
|
+
<div style="padding: 6px; background: #252525; border-radius: 3px; margin-bottom: 6px;">
|
|
645
|
+
<strong>${u.name}</strong><br>
|
|
646
|
+
<small style="color: #999;">${u.email}</small><br>
|
|
647
|
+
<small>Role: <span style="color: #0078d4;">${u.role}</span></small>
|
|
648
|
+
</div>
|
|
649
|
+
`).join('')}
|
|
650
|
+
</div>
|
|
651
|
+
`;
|
|
652
|
+
} else if (DATA.activeTab === 'activity') {
|
|
653
|
+
contentDiv.innerHTML = `
|
|
654
|
+
<div style="font-weight: bold; margin-bottom: 8px;">Activity Log</div>
|
|
655
|
+
<div style="font-size: 12px;">
|
|
656
|
+
${DATA.activityLog.length === 0 ? 'No activity yet.' : DATA.activityLog.slice().reverse().slice(0, 20).map((a, i) => `
|
|
657
|
+
<div style="padding: 6px; border-bottom: 1px solid #333;">
|
|
658
|
+
<strong>${a.action.charAt(0).toUpperCase() + a.action.slice(1)}</strong><br>
|
|
659
|
+
<small style="color: #999;">${a.timestamp.toLocaleTimeString()}</small>
|
|
660
|
+
</div>
|
|
661
|
+
`).join('')}
|
|
662
|
+
</div>
|
|
663
|
+
`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
panel.appendChild(contentDiv);
|
|
667
|
+
|
|
668
|
+
// Storage indicator
|
|
669
|
+
const storageDiv = document.createElement('div');
|
|
670
|
+
storageDiv.style.cssText = 'margin-top: 12px; border-top: 1px solid #555; padding-top: 8px; font-size: 12px;';
|
|
671
|
+
const percentUsed = Math.round((DATA.totalStorageUsed / DATA.storageQuota) * 100);
|
|
672
|
+
storageDiv.innerHTML = `
|
|
673
|
+
<div style="margin-bottom: 4px;">
|
|
674
|
+
<strong>Storage: ${percentUsed}%</strong>
|
|
675
|
+
<div style="width: 100%; height: 6px; background: #333; border-radius: 3px; overflow: hidden; margin-top: 4px;">
|
|
676
|
+
<div style="height: 100%; width: ${percentUsed}%; background: ${percentUsed > 90 ? '#d13438' : '#0078d4'};"></div>
|
|
677
|
+
</div>
|
|
678
|
+
</div>
|
|
679
|
+
<small style="color: #999;">
|
|
680
|
+
${(DATA.totalStorageUsed / 1024 / 1024).toFixed(1)} MB / ${(DATA.storageQuota / 1024 / 1024 / 1024).toFixed(1)} GB
|
|
681
|
+
</small>
|
|
682
|
+
`;
|
|
683
|
+
panel.appendChild(storageDiv);
|
|
684
|
+
|
|
685
|
+
// Close button
|
|
686
|
+
const closeBtn = document.createElement('button');
|
|
687
|
+
closeBtn.textContent = '✕';
|
|
688
|
+
closeBtn.style.cssText = `
|
|
689
|
+
position: absolute; top: 8px; right: 8px; width: 24px; height: 24px;
|
|
690
|
+
background: #d13438; color: white; border: none; border-radius: 3px;
|
|
691
|
+
cursor: pointer; font-weight: bold;
|
|
692
|
+
`;
|
|
693
|
+
closeBtn.addEventListener('click', () => {
|
|
694
|
+
DATA.panelOpen = false;
|
|
695
|
+
panel.style.display = 'none';
|
|
696
|
+
});
|
|
697
|
+
panel.appendChild(closeBtn);
|
|
698
|
+
|
|
699
|
+
return panel;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function updateUI() {
|
|
703
|
+
const panel = document.getElementById('fusion-data-panel');
|
|
704
|
+
if (panel) {
|
|
705
|
+
panel.remove();
|
|
706
|
+
const newPanel = getUI();
|
|
707
|
+
document.body.appendChild(newPanel);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ============================================================================
|
|
712
|
+
// MODULE API
|
|
713
|
+
// ============================================================================
|
|
714
|
+
|
|
715
|
+
export function init() {
|
|
716
|
+
const panel = getUI();
|
|
717
|
+
document.body.appendChild(panel);
|
|
718
|
+
|
|
719
|
+
// Auto-save every 5 minutes
|
|
720
|
+
setInterval(() => {
|
|
721
|
+
createVersion(`Auto-save ${new Date().toLocaleTimeString()}`, '', null, true);
|
|
722
|
+
}, 5 * 60 * 1000);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Public API for agent integration
|
|
727
|
+
*/
|
|
728
|
+
export function execute(command, params = {}) {
|
|
729
|
+
switch (command) {
|
|
730
|
+
case 'createProject':
|
|
731
|
+
const project = createProject(params.name, params.description);
|
|
732
|
+
return { status: 'ok', projectId: project.id, project };
|
|
733
|
+
|
|
734
|
+
case 'addFile':
|
|
735
|
+
const file = addFileToProject(params.projectId, params.fileName, params.data, params.format);
|
|
736
|
+
return { status: 'ok', fileId: file.id };
|
|
737
|
+
|
|
738
|
+
case 'deleteFile':
|
|
739
|
+
const deleted = deleteFile(params.fileId);
|
|
740
|
+
return { status: deleted ? 'ok' : 'error', deleted };
|
|
741
|
+
|
|
742
|
+
case 'createVersion':
|
|
743
|
+
const version = createVersion(params.name, params.description, params.data);
|
|
744
|
+
return { status: 'ok', versionId: version.id, version };
|
|
745
|
+
|
|
746
|
+
case 'restoreVersion':
|
|
747
|
+
const restored = restoreVersion(params.versionId);
|
|
748
|
+
return { status: restored ? 'ok' : 'error' };
|
|
749
|
+
|
|
750
|
+
case 'createBranch':
|
|
751
|
+
const created = createBranch(params.branchName);
|
|
752
|
+
return { status: created ? 'ok' : 'error', message: created ? 'Branch created' : 'Branch already exists' };
|
|
753
|
+
|
|
754
|
+
case 'switchBranch':
|
|
755
|
+
const switched = switchBranch(params.branchName);
|
|
756
|
+
return { status: switched ? 'ok' : 'error' };
|
|
757
|
+
|
|
758
|
+
case 'mergeBranch':
|
|
759
|
+
return mergeBranch(params.sourceBranch, params.targetBranch);
|
|
760
|
+
|
|
761
|
+
case 'compareVersions':
|
|
762
|
+
const diff = compareVersions(params.versionId1, params.versionId2);
|
|
763
|
+
return { status: diff ? 'ok' : 'error', diff };
|
|
764
|
+
|
|
765
|
+
case 'generateShareLink':
|
|
766
|
+
const link = generateShareLink(params.fileId, params.type, params.expiryDays);
|
|
767
|
+
return { status: 'ok', link };
|
|
768
|
+
|
|
769
|
+
case 'getEmbedCode':
|
|
770
|
+
const embedCode = getEmbedCode(params.fileId);
|
|
771
|
+
return { status: 'ok', embedCode };
|
|
772
|
+
|
|
773
|
+
case 'revokeShareLink':
|
|
774
|
+
const revoked = revokeShareLink(params.linkId);
|
|
775
|
+
return { status: revoked ? 'ok' : 'error' };
|
|
776
|
+
|
|
777
|
+
case 'addTeamMember':
|
|
778
|
+
const user = addTeamMember(params.email, params.role);
|
|
779
|
+
return { status: 'ok', userId: user.id, user };
|
|
780
|
+
|
|
781
|
+
case 'removeTeamMember':
|
|
782
|
+
const removed = removeTeamMember(params.userId);
|
|
783
|
+
return { status: removed ? 'ok' : 'error' };
|
|
784
|
+
|
|
785
|
+
case 'updateUserRole':
|
|
786
|
+
const updated = updateUserRole(params.userId, params.role);
|
|
787
|
+
return { status: updated ? 'ok' : 'error' };
|
|
788
|
+
|
|
789
|
+
case 'getProjects':
|
|
790
|
+
return { status: 'ok', projects: DATA.projects };
|
|
791
|
+
|
|
792
|
+
case 'getVersions':
|
|
793
|
+
return { status: 'ok', versions: DATA.versions };
|
|
794
|
+
|
|
795
|
+
case 'getActivity':
|
|
796
|
+
return { status: 'ok', activity: DATA.activityLog.slice(-50) };
|
|
797
|
+
|
|
798
|
+
case 'getTeam':
|
|
799
|
+
return { status: 'ok', users: DATA.users };
|
|
800
|
+
|
|
801
|
+
case 'getStorageInfo':
|
|
802
|
+
return {
|
|
803
|
+
status: 'ok',
|
|
804
|
+
used: DATA.totalStorageUsed,
|
|
805
|
+
quota: DATA.storageQuota,
|
|
806
|
+
percentUsed: Math.round((DATA.totalStorageUsed / DATA.storageQuota) * 100),
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
default:
|
|
810
|
+
return { status: 'error', message: `Unknown command: ${command}` };
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export default { init, getUI, execute };
|