@syke1/mcp-server 1.4.0 → 1.4.2
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/dist/config.d.ts +4 -0
- package/dist/config.js +23 -0
- package/dist/index.js +30 -4
- package/dist/license/validator.d.ts +4 -0
- package/dist/license/validator.js +14 -0
- package/dist/web/public/app.js +672 -28
- package/dist/web/public/index.html +62 -0
- package/dist/web/public/style.css +683 -2
- package/dist/web/server.d.ts +6 -1
- package/dist/web/server.js +17 -1
- package/package.json +1 -1
package/dist/web/public/app.js
CHANGED
|
@@ -23,6 +23,17 @@ let birthAnimations = new Map(); // nodeId → { startTime, spawnPos, targetPos
|
|
|
23
23
|
let searchActive = false; // true when search input has text
|
|
24
24
|
let _searchRAF = null; // RAF loop for search glow animation
|
|
25
25
|
|
|
26
|
+
// ── File Tree state ──
|
|
27
|
+
let fileTreeData = null; // built tree structure
|
|
28
|
+
let fileTreeExpanded = new Set(); // open folder paths
|
|
29
|
+
let fileTreeFilter = ''; // search filter
|
|
30
|
+
let fileTreeSort = 'name'; // sort mode
|
|
31
|
+
let fileTreeModified = new Map(); // path → {type, timestamp}
|
|
32
|
+
let fileTreeVisible = true; // panel visibility
|
|
33
|
+
let _fileTreeRenderTimer = null; // debounce timer
|
|
34
|
+
let _treeScrollLock = false; // manual scroll lock
|
|
35
|
+
let _treeScrollLockTimer = null; // scroll lock timer
|
|
36
|
+
|
|
26
37
|
let LAYER_HEX = {
|
|
27
38
|
FE: "#00d4ff", BE: "#c084fc", DB: "#ff6b35",
|
|
28
39
|
API: "#00ffaa", CONFIG: "#ffd700", UTIL: "#ff69b4",
|
|
@@ -97,6 +108,17 @@ const SETTINGS_DEFAULTS = {
|
|
|
97
108
|
crossLayerDistance: 900,
|
|
98
109
|
clusterStrength: 0.015,
|
|
99
110
|
},
|
|
111
|
+
fileTree: {
|
|
112
|
+
panelWidth: 280,
|
|
113
|
+
fontSize: 11,
|
|
114
|
+
indentSize: 14,
|
|
115
|
+
pulseDuration: 5,
|
|
116
|
+
showLineCount: false,
|
|
117
|
+
showDeps: true,
|
|
118
|
+
showRisk: true,
|
|
119
|
+
autoScrollOnChange: true,
|
|
120
|
+
compactMode: false,
|
|
121
|
+
},
|
|
100
122
|
};
|
|
101
123
|
|
|
102
124
|
function deepMerge(defaults, override) {
|
|
@@ -208,6 +230,9 @@ function applySettings(group) {
|
|
|
208
230
|
case "animation":
|
|
209
231
|
// Animation values are read when new nodes appear, no action needed
|
|
210
232
|
break;
|
|
233
|
+
case "fileTree":
|
|
234
|
+
applyFileTreeSettings();
|
|
235
|
+
break;
|
|
211
236
|
}
|
|
212
237
|
}
|
|
213
238
|
|
|
@@ -261,6 +286,15 @@ function setupSettings() {
|
|
|
261
286
|
["animation", "spawnX", "#set-anim-spawnX", "range"],
|
|
262
287
|
["animation", "spawnY", "#set-anim-spawnY", "range"],
|
|
263
288
|
["animation", "spawnZ", "#set-anim-spawnZ", "range"],
|
|
289
|
+
["fileTree", "panelWidth", "#set-ft-panelWidth", "range"],
|
|
290
|
+
["fileTree", "fontSize", "#set-ft-fontSize", "range"],
|
|
291
|
+
["fileTree", "indentSize", "#set-ft-indentSize", "range"],
|
|
292
|
+
["fileTree", "pulseDuration", "#set-ft-pulseDuration", "range"],
|
|
293
|
+
["fileTree", "showLineCount", "#set-ft-showLineCount", "checkbox"],
|
|
294
|
+
["fileTree", "showDeps", "#set-ft-showDeps", "checkbox"],
|
|
295
|
+
["fileTree", "showRisk", "#set-ft-showRisk", "checkbox"],
|
|
296
|
+
["fileTree", "autoScrollOnChange", "#set-ft-autoScrollOnChange", "checkbox"],
|
|
297
|
+
["fileTree", "compactMode", "#set-ft-compactMode", "checkbox"],
|
|
264
298
|
];
|
|
265
299
|
|
|
266
300
|
// Init values and bind events
|
|
@@ -268,16 +302,25 @@ function setupSettings() {
|
|
|
268
302
|
const el = document.querySelector(selector);
|
|
269
303
|
if (!el) continue;
|
|
270
304
|
const valEl = el.parentElement?.querySelector(".set-val");
|
|
271
|
-
el.value = SETTINGS[group][key];
|
|
272
|
-
if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
273
305
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
306
|
+
if (type === "checkbox") {
|
|
307
|
+
el.checked = !!SETTINGS[group][key];
|
|
308
|
+
el.addEventListener("change", () => {
|
|
309
|
+
SETTINGS[group][key] = el.checked;
|
|
310
|
+
applySettings(group);
|
|
311
|
+
saveSettings();
|
|
312
|
+
});
|
|
313
|
+
} else {
|
|
314
|
+
el.value = SETTINGS[group][key];
|
|
315
|
+
if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
316
|
+
el.addEventListener("input", () => {
|
|
317
|
+
const v = type === "color" ? el.value : parseFloat(el.value);
|
|
318
|
+
SETTINGS[group][key] = v;
|
|
319
|
+
if (valEl) valEl.textContent = formatSetVal(v, type);
|
|
320
|
+
applySettings(group);
|
|
321
|
+
saveSettings();
|
|
322
|
+
});
|
|
323
|
+
}
|
|
281
324
|
}
|
|
282
325
|
|
|
283
326
|
// Collapsible sections
|
|
@@ -302,9 +345,10 @@ function setupSettings() {
|
|
|
302
345
|
if (g !== group) continue;
|
|
303
346
|
const el = document.querySelector(selector);
|
|
304
347
|
if (!el) continue;
|
|
305
|
-
el.
|
|
348
|
+
if (type === "checkbox") { el.checked = !!SETTINGS[group][key]; }
|
|
349
|
+
else { el.value = SETTINGS[group][key]; }
|
|
306
350
|
const valEl = el.parentElement?.querySelector(".set-val");
|
|
307
|
-
if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
351
|
+
if (valEl && type !== "checkbox") valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
308
352
|
}
|
|
309
353
|
});
|
|
310
354
|
});
|
|
@@ -319,9 +363,10 @@ function setupSettings() {
|
|
|
319
363
|
for (const [group, key, selector, type] of CTRL_MAP) {
|
|
320
364
|
const el = document.querySelector(selector);
|
|
321
365
|
if (!el) continue;
|
|
322
|
-
el.
|
|
366
|
+
if (type === "checkbox") { el.checked = !!SETTINGS[group][key]; }
|
|
367
|
+
else { el.value = SETTINGS[group][key]; }
|
|
323
368
|
const valEl = el.parentElement?.querySelector(".set-val");
|
|
324
|
-
if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
369
|
+
if (valEl && type !== "checkbox") valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
325
370
|
}
|
|
326
371
|
});
|
|
327
372
|
}
|
|
@@ -365,9 +410,10 @@ function setupSettings() {
|
|
|
365
410
|
for (const [group, key, selector, type] of CTRL_MAP) {
|
|
366
411
|
const el = document.querySelector(selector);
|
|
367
412
|
if (!el) continue;
|
|
368
|
-
el.
|
|
413
|
+
if (type === "checkbox") { el.checked = !!SETTINGS[group][key]; }
|
|
414
|
+
else { el.value = SETTINGS[group][key]; }
|
|
369
415
|
const valEl = el.parentElement?.querySelector(".set-val");
|
|
370
|
-
if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
416
|
+
if (valEl && type !== "checkbox") valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
371
417
|
}
|
|
372
418
|
} catch(e) {
|
|
373
419
|
console.error("[SYKE] Import failed:", e);
|
|
@@ -417,6 +463,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
417
463
|
setupTabs();
|
|
418
464
|
setupSettings();
|
|
419
465
|
setupProjectModal();
|
|
466
|
+
setupLicenseModal();
|
|
467
|
+
setupFileTree();
|
|
420
468
|
initSSE();
|
|
421
469
|
startHealthCheck();
|
|
422
470
|
});
|
|
@@ -548,6 +596,7 @@ async function loadGraph() {
|
|
|
548
596
|
if (isReload) {
|
|
549
597
|
Graph.graphData(graphData);
|
|
550
598
|
buildLegend(layerCounts);
|
|
599
|
+
renderFileTreeDebounced();
|
|
551
600
|
console.log("[SYKE] Graph updated (reload), birth animations:", birthAnimations.size);
|
|
552
601
|
|
|
553
602
|
// ── Star birth camera choreography ──
|
|
@@ -595,7 +644,7 @@ async function loadGraph() {
|
|
|
595
644
|
const container = document.getElementById("3d-graph");
|
|
596
645
|
|
|
597
646
|
Graph = ForceGraph3D()(container)
|
|
598
|
-
.width(
|
|
647
|
+
.width(getGraphPanelWidth())
|
|
599
648
|
.height(window.innerHeight - 100)
|
|
600
649
|
.graphData(graphData)
|
|
601
650
|
.backgroundColor(SETTINGS.scene.background)
|
|
@@ -757,10 +806,11 @@ async function loadGraph() {
|
|
|
757
806
|
container.addEventListener("touchstart", stopUser);
|
|
758
807
|
|
|
759
808
|
window.addEventListener("resize", () => {
|
|
760
|
-
if (Graph) Graph.width(
|
|
809
|
+
if (Graph) Graph.width(getGraphPanelWidth()).height(window.innerHeight - 100);
|
|
761
810
|
});
|
|
762
811
|
|
|
763
812
|
buildLegend(layerCounts);
|
|
813
|
+
renderFileTreeDebounced();
|
|
764
814
|
createNodeLabels();
|
|
765
815
|
updateLabelsLoop();
|
|
766
816
|
|
|
@@ -1141,6 +1191,8 @@ async function handleNodeClick(node) {
|
|
|
1141
1191
|
loadSimulation(node.id);
|
|
1142
1192
|
// Start Star Wars code crawl
|
|
1143
1193
|
startCodeCrawl(node.id);
|
|
1194
|
+
// Sync file tree selection
|
|
1195
|
+
treeScrollToFile(node.id);
|
|
1144
1196
|
}
|
|
1145
1197
|
|
|
1146
1198
|
function handleBackgroundClick() {
|
|
@@ -1844,6 +1896,9 @@ function setupKeyboardShortcuts() {
|
|
|
1844
1896
|
case "s":
|
|
1845
1897
|
if (selectedFile) { loadSimulation(selectedFile); switchTab("simulate"); }
|
|
1846
1898
|
break;
|
|
1899
|
+
case "t":
|
|
1900
|
+
toggleFileTreePanel();
|
|
1901
|
+
break;
|
|
1847
1902
|
case "d":
|
|
1848
1903
|
detectCycles();
|
|
1849
1904
|
break;
|
|
@@ -2059,8 +2114,9 @@ function setupEventListeners() {
|
|
|
2059
2114
|
});
|
|
2060
2115
|
});
|
|
2061
2116
|
|
|
2062
|
-
// ── Resizable
|
|
2117
|
+
// ── Resizable Panels ──
|
|
2063
2118
|
setupResizeHandle();
|
|
2119
|
+
setupTreeResizeHandle();
|
|
2064
2120
|
}
|
|
2065
2121
|
|
|
2066
2122
|
// ═══════════════════════════════════════════
|
|
@@ -2707,6 +2763,16 @@ async function initSSE() {
|
|
|
2707
2763
|
const data = JSON.parse(e.data);
|
|
2708
2764
|
console.log("[SYKE:SSE] File change:", data.file, data.type, data.diffCount, "diffs");
|
|
2709
2765
|
|
|
2766
|
+
// ── 0. Update file tree modification tracking ──
|
|
2767
|
+
fileTreeModified.set(data.file, { type: data.type, timestamp: Date.now() });
|
|
2768
|
+
renderFileTreeDebounced();
|
|
2769
|
+
// Auto-clear pulse after pulseDuration
|
|
2770
|
+
const pulseDur = (SETTINGS.fileTree.pulseDuration || 5) * 1000;
|
|
2771
|
+
setTimeout(() => {
|
|
2772
|
+
fileTreeModified.delete(data.file);
|
|
2773
|
+
renderFileTreeDebounced();
|
|
2774
|
+
}, pulseDur);
|
|
2775
|
+
|
|
2710
2776
|
// ── 1. Auto-select the modified node in 3D graph ──
|
|
2711
2777
|
selectedFile = data.file;
|
|
2712
2778
|
selectedNodeId = data.file;
|
|
@@ -2861,6 +2927,7 @@ async function initSSE() {
|
|
|
2861
2927
|
setTimeout(async () => {
|
|
2862
2928
|
await loadGraph();
|
|
2863
2929
|
await loadHubFiles();
|
|
2930
|
+
renderFileTreeDebounced();
|
|
2864
2931
|
}, 1000);
|
|
2865
2932
|
});
|
|
2866
2933
|
|
|
@@ -2980,6 +3047,489 @@ function renderRealtimePanel() {
|
|
|
2980
3047
|
// ═══════════════════════════════════════════
|
|
2981
3048
|
// RESIZABLE RIGHT PANEL
|
|
2982
3049
|
// ═══════════════════════════════════════════
|
|
3050
|
+
// ═══════════════════════════════════════════
|
|
3051
|
+
// FILE TREE PANEL
|
|
3052
|
+
// ═══════════════════════════════════════════
|
|
3053
|
+
|
|
3054
|
+
function getGraphPanelWidth() {
|
|
3055
|
+
const rightPanel = document.getElementById("right-panel");
|
|
3056
|
+
const treePanel = document.getElementById("file-tree-panel");
|
|
3057
|
+
const rw = rightPanel ? rightPanel.offsetWidth : 380;
|
|
3058
|
+
const tw = (treePanel && !treePanel.classList.contains("hidden")) ? treePanel.offsetWidth : 0;
|
|
3059
|
+
// 8px per resize handle (tree + right)
|
|
3060
|
+
return window.innerWidth - rw - tw - 16;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
function updateDynamicRightOffsets() {
|
|
3064
|
+
const rightPanel = document.getElementById("right-panel");
|
|
3065
|
+
const treePanel = document.getElementById("file-tree-panel");
|
|
3066
|
+
const rw = rightPanel ? rightPanel.offsetWidth : 380;
|
|
3067
|
+
const tw = (treePanel && !treePanel.classList.contains("hidden")) ? treePanel.offsetWidth : 0;
|
|
3068
|
+
const total = rw + tw + 16; // 2 resize handles × 8px
|
|
3069
|
+
document.documentElement.style.setProperty("--right-offset", total + "px");
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
function toggleFileTreePanel() {
|
|
3073
|
+
const panel = document.getElementById("file-tree-panel");
|
|
3074
|
+
const treeResize = document.getElementById("tree-resize-handle");
|
|
3075
|
+
if (!panel) return;
|
|
3076
|
+
fileTreeVisible = !fileTreeVisible;
|
|
3077
|
+
panel.classList.toggle("hidden", !fileTreeVisible);
|
|
3078
|
+
if (treeResize) treeResize.style.display = fileTreeVisible ? "" : "none";
|
|
3079
|
+
updateDynamicRightOffsets();
|
|
3080
|
+
if (Graph) {
|
|
3081
|
+
const gp = document.getElementById("graph-panel");
|
|
3082
|
+
if (gp) Graph.width(gp.clientWidth);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
function buildFileTree(nodes) {
|
|
3087
|
+
if (!nodes || !nodes.length) { fileTreeData = null; return; }
|
|
3088
|
+
const root = { name: "", children: {}, files: [], path: "" };
|
|
3089
|
+
|
|
3090
|
+
nodes.forEach(n => {
|
|
3091
|
+
const parts = (n.fullPath || n.id).split("/").filter(Boolean);
|
|
3092
|
+
let current = root;
|
|
3093
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
3094
|
+
const folderName = parts[i];
|
|
3095
|
+
const folderPath = parts.slice(0, i + 1).join("/");
|
|
3096
|
+
if (!current.children[folderName]) {
|
|
3097
|
+
current.children[folderName] = { name: folderName, children: {}, files: [], path: folderPath };
|
|
3098
|
+
}
|
|
3099
|
+
current = current.children[folderName];
|
|
3100
|
+
}
|
|
3101
|
+
current.files.push({
|
|
3102
|
+
name: parts[parts.length - 1] || n.label,
|
|
3103
|
+
node: n,
|
|
3104
|
+
path: n.fullPath || n.id,
|
|
3105
|
+
});
|
|
3106
|
+
});
|
|
3107
|
+
|
|
3108
|
+
fileTreeData = root;
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
function countFilesRecursive(folder) {
|
|
3112
|
+
let count = folder.files.length;
|
|
3113
|
+
for (const child of Object.values(folder.children)) {
|
|
3114
|
+
count += countFilesRecursive(child);
|
|
3115
|
+
}
|
|
3116
|
+
return count;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
function getAggregateRisk(folder) {
|
|
3120
|
+
const order = ["HIGH", "MEDIUM", "LOW", "NONE"];
|
|
3121
|
+
let highest = 3; // NONE
|
|
3122
|
+
for (const f of folder.files) {
|
|
3123
|
+
const idx = order.indexOf(f.node.riskLevel);
|
|
3124
|
+
if (idx >= 0 && idx < highest) highest = idx;
|
|
3125
|
+
}
|
|
3126
|
+
for (const child of Object.values(folder.children)) {
|
|
3127
|
+
const childRisk = getAggregateRisk(child);
|
|
3128
|
+
const idx = order.indexOf(childRisk);
|
|
3129
|
+
if (idx >= 0 && idx < highest) highest = idx;
|
|
3130
|
+
}
|
|
3131
|
+
return order[highest];
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
function folderHasModifiedChild(folder) {
|
|
3135
|
+
for (const f of folder.files) {
|
|
3136
|
+
if (fileTreeModified.has(f.path)) return true;
|
|
3137
|
+
}
|
|
3138
|
+
for (const child of Object.values(folder.children)) {
|
|
3139
|
+
if (folderHasModifiedChild(child)) return true;
|
|
3140
|
+
}
|
|
3141
|
+
return false;
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
function folderMatchesFilter(folder, filter) {
|
|
3145
|
+
if (!filter) return true;
|
|
3146
|
+
const fl = filter.toLowerCase();
|
|
3147
|
+
for (const f of folder.files) {
|
|
3148
|
+
if (f.name.toLowerCase().includes(fl) || f.path.toLowerCase().includes(fl)) return true;
|
|
3149
|
+
}
|
|
3150
|
+
for (const child of Object.values(folder.children)) {
|
|
3151
|
+
if (child.name.toLowerCase().includes(fl) || folderMatchesFilter(child, filter)) return true;
|
|
3152
|
+
}
|
|
3153
|
+
return false;
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
function sortTreeItems(items, sortType) {
|
|
3157
|
+
return [...items].sort((a, b) => {
|
|
3158
|
+
switch (sortType) {
|
|
3159
|
+
case "layer": {
|
|
3160
|
+
const la = a.node ? (a.node.layer || "UTIL") : "";
|
|
3161
|
+
const lb = b.node ? (b.node.layer || "UTIL") : "";
|
|
3162
|
+
return la.localeCompare(lb) || (a.name || "").localeCompare(b.name || "");
|
|
3163
|
+
}
|
|
3164
|
+
case "risk": {
|
|
3165
|
+
const order = { HIGH: 0, MEDIUM: 1, LOW: 2, NONE: 3 };
|
|
3166
|
+
const ra = a.node ? (order[a.node.riskLevel] ?? 3) : 3;
|
|
3167
|
+
const rb = b.node ? (order[b.node.riskLevel] ?? 3) : 3;
|
|
3168
|
+
return ra - rb || (a.name || "").localeCompare(b.name || "");
|
|
3169
|
+
}
|
|
3170
|
+
case "deps": {
|
|
3171
|
+
const da = a.node ? (a.node.dependentCount || 0) : 0;
|
|
3172
|
+
const db = b.node ? (b.node.dependentCount || 0) : 0;
|
|
3173
|
+
return db - da || (a.name || "").localeCompare(b.name || "");
|
|
3174
|
+
}
|
|
3175
|
+
case "modified": {
|
|
3176
|
+
const ma = a.path && fileTreeModified.has(a.path) ? fileTreeModified.get(a.path).timestamp : 0;
|
|
3177
|
+
const mb = b.path && fileTreeModified.has(b.path) ? fileTreeModified.get(b.path).timestamp : 0;
|
|
3178
|
+
return mb - ma || (a.name || "").localeCompare(b.name || "");
|
|
3179
|
+
}
|
|
3180
|
+
default: // name
|
|
3181
|
+
return (a.name || "").localeCompare(b.name || "");
|
|
3182
|
+
}
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
function renderFileTree() {
|
|
3187
|
+
if (!graphData || !graphData.nodes) return;
|
|
3188
|
+
buildFileTree(graphData.nodes);
|
|
3189
|
+
if (!fileTreeData) return;
|
|
3190
|
+
|
|
3191
|
+
const container = document.getElementById("tree-content");
|
|
3192
|
+
const countEl = document.getElementById("tree-file-count");
|
|
3193
|
+
if (!container) return;
|
|
3194
|
+
|
|
3195
|
+
const s = SETTINGS.fileTree;
|
|
3196
|
+
const filter = fileTreeFilter.toLowerCase();
|
|
3197
|
+
let totalFiles = 0;
|
|
3198
|
+
|
|
3199
|
+
// Nested rendering: each folder level produces a <div class="tree-group">
|
|
3200
|
+
function renderGroup(folder) {
|
|
3201
|
+
// Collect visible items (folders first, then files)
|
|
3202
|
+
const visibleFolders = Object.values(folder.children)
|
|
3203
|
+
.filter(c => !filter || folderMatchesFilter(c, filter))
|
|
3204
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
3205
|
+
|
|
3206
|
+
const visibleFiles = sortTreeItems(
|
|
3207
|
+
folder.files.filter(f => !filter || f.name.toLowerCase().includes(filter) || f.path.toLowerCase().includes(filter)),
|
|
3208
|
+
fileTreeSort
|
|
3209
|
+
);
|
|
3210
|
+
|
|
3211
|
+
const itemCount = visibleFolders.length + visibleFiles.length;
|
|
3212
|
+
if (itemCount === 0) return "";
|
|
3213
|
+
|
|
3214
|
+
const hasModified = folderHasModifiedChild(folder);
|
|
3215
|
+
const singleClass = itemCount === 1 ? " single-child" : "";
|
|
3216
|
+
const modGroupClass = hasModified ? " has-modified" : "";
|
|
3217
|
+
let html = `<div class="tree-group${singleClass}${modGroupClass}">`;
|
|
3218
|
+
|
|
3219
|
+
// ── Folders ──
|
|
3220
|
+
for (const child of visibleFolders) {
|
|
3221
|
+
const isOpen = fileTreeExpanded.has(child.path);
|
|
3222
|
+
const fileCount = countFilesRecursive(child);
|
|
3223
|
+
const hasModChild = folderHasModifiedChild(child);
|
|
3224
|
+
const compactClass = s.compactMode ? " compact" : "";
|
|
3225
|
+
const glowClass = hasModChild ? " folder-glow" : "";
|
|
3226
|
+
const col = "var(--accent)";
|
|
3227
|
+
|
|
3228
|
+
html += `<div class="tree-node${glowClass}${compactClass}" data-path="${child.path}" data-type="folder">`;
|
|
3229
|
+
html += `<div class="tree-dot" style="border-color:${col};box-shadow:0 0 8px rgba(0,212,255,0.4)"></div>`;
|
|
3230
|
+
html += `<span class="tree-toggle${isOpen ? " open" : ""}" data-path="${child.path}">▶</span>`;
|
|
3231
|
+
html += `<span class="tree-name folder-name">${escHtml(child.name)}</span>`;
|
|
3232
|
+
html += `<span class="tree-folder-count">${fileCount}</span>`;
|
|
3233
|
+
html += `</div>`;
|
|
3234
|
+
|
|
3235
|
+
// Render expanded children as nested tree-group
|
|
3236
|
+
if (isOpen) {
|
|
3237
|
+
html += renderGroup(child);
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
// ── Files ──
|
|
3242
|
+
for (const f of visibleFiles) {
|
|
3243
|
+
totalFiles++;
|
|
3244
|
+
const isSelected = selectedNodeId === f.node.id;
|
|
3245
|
+
const isModified = fileTreeModified.has(f.path);
|
|
3246
|
+
const modData = isModified ? fileTreeModified.get(f.path) : null;
|
|
3247
|
+
const col = LAYER_HEX[f.node.layer] || "#999";
|
|
3248
|
+
const compactClass = s.compactMode ? " compact" : "";
|
|
3249
|
+
const selClass = isSelected ? " selected" : "";
|
|
3250
|
+
const modClass = isModified ? " modified" : "";
|
|
3251
|
+
|
|
3252
|
+
html += `<div class="tree-node${selClass}${modClass}${compactClass}" style="font-size:${s.fontSize}px" data-path="${f.path}" data-node-id="${f.node.id}" data-type="file">`;
|
|
3253
|
+
html += `<div class="tree-dot" style="background:${col};border-color:${col};box-shadow:0 0 8px ${col}"></div>`;
|
|
3254
|
+
html += `<span class="tree-name">${escHtml(f.name)}</span>`;
|
|
3255
|
+
|
|
3256
|
+
// Badges
|
|
3257
|
+
if (isModified && modData) {
|
|
3258
|
+
const modLabel = modData.type === "added" ? "A" : modData.type === "deleted" ? "D" : "M";
|
|
3259
|
+
const modCls = modData.type === "added" ? "mod-A" : modData.type === "deleted" ? "mod-D" : "mod-M";
|
|
3260
|
+
html += `<span class="tree-mod-badge ${modCls}">${modLabel}</span>`;
|
|
3261
|
+
}
|
|
3262
|
+
if (s.showRisk && f.node.riskLevel && f.node.riskLevel !== "NONE") {
|
|
3263
|
+
html += `<span class="tree-badge badge-risk-${f.node.riskLevel}">${f.node.riskLevel[0]}</span>`;
|
|
3264
|
+
}
|
|
3265
|
+
if (s.showDeps && f.node.dependentCount > 0) {
|
|
3266
|
+
html += `<span class="tree-badge badge-deps">D${f.node.dependentCount}</span>`;
|
|
3267
|
+
}
|
|
3268
|
+
if (s.showLineCount && f.node.lineCount > 0) {
|
|
3269
|
+
html += `<span class="tree-badge badge-lines">L${f.node.lineCount}</span>`;
|
|
3270
|
+
}
|
|
3271
|
+
html += `</div>`;
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
html += `</div>`; // close tree-group
|
|
3275
|
+
return html;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
container.innerHTML = renderGroup(fileTreeData);
|
|
3279
|
+
if (countEl) countEl.textContent = totalFiles;
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
function escHtml(s) {
|
|
3283
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
function renderFileTreeDebounced() {
|
|
3287
|
+
if (_fileTreeRenderTimer) clearTimeout(_fileTreeRenderTimer);
|
|
3288
|
+
_fileTreeRenderTimer = setTimeout(() => {
|
|
3289
|
+
renderFileTree();
|
|
3290
|
+
_fileTreeRenderTimer = null;
|
|
3291
|
+
}, 100);
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
function toggleFolder(path) {
|
|
3295
|
+
if (fileTreeExpanded.has(path)) fileTreeExpanded.delete(path);
|
|
3296
|
+
else fileTreeExpanded.add(path);
|
|
3297
|
+
renderFileTree();
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
function expandAllFolders() {
|
|
3301
|
+
if (!fileTreeData) return;
|
|
3302
|
+
function collectPaths(folder) {
|
|
3303
|
+
for (const child of Object.values(folder.children)) {
|
|
3304
|
+
fileTreeExpanded.add(child.path);
|
|
3305
|
+
collectPaths(child);
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
collectPaths(fileTreeData);
|
|
3309
|
+
renderFileTree();
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
function collapseAllFolders() {
|
|
3313
|
+
fileTreeExpanded.clear();
|
|
3314
|
+
renderFileTree();
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
function treeScrollToFile(nodeId) {
|
|
3318
|
+
if (!fileTreeData || !nodeId || _treeScrollLock) return;
|
|
3319
|
+
// Find file path and expand parent folders
|
|
3320
|
+
const node = graphData?.nodes.find(n => n.id === nodeId);
|
|
3321
|
+
if (!node) return;
|
|
3322
|
+
const fullPath = node.fullPath || nodeId;
|
|
3323
|
+
const parts = fullPath.split("/").filter(Boolean);
|
|
3324
|
+
|
|
3325
|
+
// Expand all parent folders
|
|
3326
|
+
for (let i = 1; i < parts.length; i++) {
|
|
3327
|
+
const folderPath = parts.slice(0, i).join("/");
|
|
3328
|
+
fileTreeExpanded.add(folderPath);
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
renderFileTree();
|
|
3332
|
+
|
|
3333
|
+
// Scroll to the file element
|
|
3334
|
+
requestAnimationFrame(() => {
|
|
3335
|
+
const container = document.getElementById("tree-scroll-container");
|
|
3336
|
+
const el = document.querySelector(`[data-node-id="${CSS.escape(nodeId)}"]`);
|
|
3337
|
+
if (el && container && SETTINGS.fileTree.autoScrollOnChange) {
|
|
3338
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
3339
|
+
}
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
function applyFileTreeSettings() {
|
|
3344
|
+
const panel = document.getElementById("file-tree-panel");
|
|
3345
|
+
if (panel) {
|
|
3346
|
+
const w = SETTINGS.fileTree.panelWidth;
|
|
3347
|
+
panel.style.flex = `0 0 ${w}px`;
|
|
3348
|
+
panel.style.width = `${w}px`;
|
|
3349
|
+
}
|
|
3350
|
+
updateDynamicRightOffsets();
|
|
3351
|
+
renderFileTree();
|
|
3352
|
+
if (Graph) {
|
|
3353
|
+
const gp = document.getElementById("graph-panel");
|
|
3354
|
+
if (gp) Graph.width(gp.clientWidth);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
function setupFileTree() {
|
|
3359
|
+
const container = document.getElementById("tree-content");
|
|
3360
|
+
const searchInput = document.getElementById("tree-search-input");
|
|
3361
|
+
const sortBtn = document.getElementById("tree-sort-btn");
|
|
3362
|
+
const sortMenu = document.getElementById("tree-sort-menu");
|
|
3363
|
+
const collapseBtn = document.getElementById("tree-collapse-all");
|
|
3364
|
+
const scrollContainer = document.getElementById("tree-scroll-container");
|
|
3365
|
+
|
|
3366
|
+
if (!container) return;
|
|
3367
|
+
|
|
3368
|
+
// Click delegation for tree nodes
|
|
3369
|
+
container.addEventListener("click", (e) => {
|
|
3370
|
+
const node = e.target.closest(".tree-node");
|
|
3371
|
+
if (!node) return;
|
|
3372
|
+
|
|
3373
|
+
if (node.dataset.type === "folder") {
|
|
3374
|
+
toggleFolder(node.dataset.path);
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
// File click → select in 3D graph
|
|
3379
|
+
const nodeId = node.dataset.nodeId;
|
|
3380
|
+
if (nodeId && graphData) {
|
|
3381
|
+
const gNode = graphData.nodes.find(n => n.id === nodeId);
|
|
3382
|
+
if (gNode) {
|
|
3383
|
+
handleNodeClick(gNode);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
});
|
|
3387
|
+
|
|
3388
|
+
// Toggle arrow click
|
|
3389
|
+
container.addEventListener("click", (e) => {
|
|
3390
|
+
const toggle = e.target.closest(".tree-toggle");
|
|
3391
|
+
if (toggle) {
|
|
3392
|
+
e.stopPropagation();
|
|
3393
|
+
toggleFolder(toggle.dataset.path);
|
|
3394
|
+
}
|
|
3395
|
+
});
|
|
3396
|
+
|
|
3397
|
+
// Hover → highlight 3D node
|
|
3398
|
+
container.addEventListener("mouseenter", (e) => {
|
|
3399
|
+
const node = e.target.closest(".tree-node[data-type='file']");
|
|
3400
|
+
if (!node || !Graph) return;
|
|
3401
|
+
const nodeId = node.dataset.nodeId;
|
|
3402
|
+
if (nodeId) {
|
|
3403
|
+
highlightNodes.add(nodeId);
|
|
3404
|
+
refreshGraph();
|
|
3405
|
+
}
|
|
3406
|
+
}, true);
|
|
3407
|
+
|
|
3408
|
+
container.addEventListener("mouseleave", (e) => {
|
|
3409
|
+
const node = e.target.closest(".tree-node[data-type='file']");
|
|
3410
|
+
if (!node || !Graph) return;
|
|
3411
|
+
const nodeId = node.dataset.nodeId;
|
|
3412
|
+
if (nodeId && nodeId !== selectedNodeId) {
|
|
3413
|
+
highlightNodes.delete(nodeId);
|
|
3414
|
+
refreshGraph();
|
|
3415
|
+
}
|
|
3416
|
+
}, true);
|
|
3417
|
+
|
|
3418
|
+
// Search filter
|
|
3419
|
+
if (searchInput) {
|
|
3420
|
+
searchInput.addEventListener("input", () => {
|
|
3421
|
+
fileTreeFilter = searchInput.value.trim();
|
|
3422
|
+
// Auto-expand when filtering
|
|
3423
|
+
if (fileTreeFilter && fileTreeData) expandAllFolders();
|
|
3424
|
+
else renderFileTree();
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
// Sort button + menu
|
|
3429
|
+
if (sortBtn && sortMenu) {
|
|
3430
|
+
sortBtn.addEventListener("click", (e) => {
|
|
3431
|
+
e.stopPropagation();
|
|
3432
|
+
sortMenu.classList.toggle("hidden");
|
|
3433
|
+
});
|
|
3434
|
+
|
|
3435
|
+
sortMenu.querySelectorAll(".tree-sort-option").forEach(opt => {
|
|
3436
|
+
opt.addEventListener("click", (e) => {
|
|
3437
|
+
e.stopPropagation();
|
|
3438
|
+
fileTreeSort = opt.dataset.sort;
|
|
3439
|
+
sortMenu.querySelectorAll(".tree-sort-option").forEach(o => o.classList.remove("active"));
|
|
3440
|
+
opt.classList.add("active");
|
|
3441
|
+
sortMenu.classList.add("hidden");
|
|
3442
|
+
renderFileTree();
|
|
3443
|
+
});
|
|
3444
|
+
});
|
|
3445
|
+
|
|
3446
|
+
// Close sort menu on outside click
|
|
3447
|
+
document.addEventListener("click", () => {
|
|
3448
|
+
if (sortMenu) sortMenu.classList.add("hidden");
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
// Collapse/expand all toggle
|
|
3453
|
+
let allExpanded = false;
|
|
3454
|
+
if (collapseBtn) {
|
|
3455
|
+
collapseBtn.addEventListener("click", () => {
|
|
3456
|
+
if (allExpanded) { collapseAllFolders(); collapseBtn.innerHTML = "▼"; }
|
|
3457
|
+
else { expandAllFolders(); collapseBtn.innerHTML = "▲"; }
|
|
3458
|
+
allExpanded = !allExpanded;
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
// Scroll lock: disable auto-scroll when user manually scrolls
|
|
3463
|
+
if (scrollContainer) {
|
|
3464
|
+
scrollContainer.addEventListener("scroll", () => {
|
|
3465
|
+
_treeScrollLock = true;
|
|
3466
|
+
if (_treeScrollLockTimer) clearTimeout(_treeScrollLockTimer);
|
|
3467
|
+
_treeScrollLockTimer = setTimeout(() => { _treeScrollLock = false; }, 3000);
|
|
3468
|
+
}, { passive: true });
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
// Apply initial settings
|
|
3472
|
+
applyFileTreeSettings();
|
|
3473
|
+
updateDynamicRightOffsets();
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
function setupTreeResizeHandle() {
|
|
3477
|
+
const handle = document.getElementById("tree-resize-handle");
|
|
3478
|
+
const panel = document.getElementById("file-tree-panel");
|
|
3479
|
+
if (!handle || !panel) return;
|
|
3480
|
+
|
|
3481
|
+
let startX = 0;
|
|
3482
|
+
let startW = 0;
|
|
3483
|
+
|
|
3484
|
+
function onMove(e) {
|
|
3485
|
+
e.preventDefault();
|
|
3486
|
+
const dx = e.clientX - startX;
|
|
3487
|
+
const newW = Math.max(200, Math.min(500, startW + dx));
|
|
3488
|
+
panel.style.flex = "0 0 " + newW + "px";
|
|
3489
|
+
panel.style.width = newW + "px";
|
|
3490
|
+
SETTINGS.fileTree.panelWidth = newW;
|
|
3491
|
+
updateDynamicRightOffsets();
|
|
3492
|
+
if (Graph) {
|
|
3493
|
+
const gp = document.getElementById("graph-panel");
|
|
3494
|
+
if (gp) Graph.width(gp.clientWidth);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
function onUp() {
|
|
3499
|
+
window.removeEventListener("mousemove", onMove, true);
|
|
3500
|
+
window.removeEventListener("mouseup", onUp, true);
|
|
3501
|
+
handle.classList.remove("dragging");
|
|
3502
|
+
document.body.style.cursor = "";
|
|
3503
|
+
document.body.style.userSelect = "";
|
|
3504
|
+
const overlay = document.getElementById("tree-resize-overlay");
|
|
3505
|
+
if (overlay) overlay.remove();
|
|
3506
|
+
saveSettings();
|
|
3507
|
+
if (Graph) {
|
|
3508
|
+
const gp = document.getElementById("graph-panel");
|
|
3509
|
+
if (gp) Graph.width(gp.clientWidth);
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
handle.addEventListener("mousedown", (e) => {
|
|
3514
|
+
e.preventDefault();
|
|
3515
|
+
e.stopPropagation();
|
|
3516
|
+
startX = e.clientX;
|
|
3517
|
+
startW = panel.offsetWidth;
|
|
3518
|
+
handle.classList.add("dragging");
|
|
3519
|
+
document.body.style.cursor = "col-resize";
|
|
3520
|
+
document.body.style.userSelect = "none";
|
|
3521
|
+
const overlay = document.createElement("div");
|
|
3522
|
+
overlay.id = "tree-resize-overlay";
|
|
3523
|
+
overlay.style.cssText = "position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:9999;cursor:col-resize;background:transparent;";
|
|
3524
|
+
document.body.appendChild(overlay);
|
|
3525
|
+
window.addEventListener("mousemove", onMove, true);
|
|
3526
|
+
window.addEventListener("mouseup", onUp, true);
|
|
3527
|
+
});
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
// ═══════════════════════════════════════════
|
|
3531
|
+
// RESIZE HANDLE (Right Panel)
|
|
3532
|
+
// ═══════════════════════════════════════════
|
|
2983
3533
|
function setupResizeHandle() {
|
|
2984
3534
|
const handle = document.getElementById("resize-handle");
|
|
2985
3535
|
const panel = document.getElementById("right-panel");
|
|
@@ -2995,7 +3545,7 @@ function setupResizeHandle() {
|
|
|
2995
3545
|
const newW = Math.max(250, Math.min(900, startW + dx));
|
|
2996
3546
|
panel.style.flex = "0 0 " + newW + "px";
|
|
2997
3547
|
panel.style.width = newW + "px";
|
|
2998
|
-
|
|
3548
|
+
updateDynamicRightOffsets();
|
|
2999
3549
|
if (Graph) {
|
|
3000
3550
|
const gp = document.getElementById("graph-panel");
|
|
3001
3551
|
if (gp) Graph.width(gp.clientWidth);
|
|
@@ -3045,21 +3595,14 @@ function updateLicenseBadge(plan, expiresAt) {
|
|
|
3045
3595
|
|
|
3046
3596
|
badge.className = "license-badge";
|
|
3047
3597
|
|
|
3048
|
-
if (plan === "pro"
|
|
3049
|
-
|
|
3050
|
-
if (diffMs <= 0) {
|
|
3598
|
+
if (plan === "pro") {
|
|
3599
|
+
if (expiresAt && new Date(expiresAt) < new Date()) {
|
|
3051
3600
|
badge.classList.add("expired");
|
|
3052
3601
|
badge.textContent = "EXPIRED";
|
|
3053
|
-
} else if (Math.ceil(diffMs / (1000 * 60 * 60 * 24)) <= 14) {
|
|
3054
|
-
badge.classList.add("trial");
|
|
3055
|
-
badge.textContent = "TRIAL";
|
|
3056
3602
|
} else {
|
|
3057
3603
|
badge.classList.add("pro");
|
|
3058
3604
|
badge.textContent = "PRO";
|
|
3059
3605
|
}
|
|
3060
|
-
} else if (plan === "pro") {
|
|
3061
|
-
badge.classList.add("pro");
|
|
3062
|
-
badge.textContent = "PRO";
|
|
3063
3606
|
} else {
|
|
3064
3607
|
badge.classList.add("free");
|
|
3065
3608
|
badge.textContent = "FREE";
|
|
@@ -3119,6 +3662,7 @@ async function loadProjectInfo() {
|
|
|
3119
3662
|
}
|
|
3120
3663
|
hideServerOffline();
|
|
3121
3664
|
updateLicenseBadge(info.plan, info.expiresAt);
|
|
3665
|
+
updateLicenseButton(info.plan);
|
|
3122
3666
|
// Bottom bar: fetch version from npm registry
|
|
3123
3667
|
updateBottomBar();
|
|
3124
3668
|
} catch (e) {
|
|
@@ -3260,6 +3804,106 @@ async function switchProject(projectPath) {
|
|
|
3260
3804
|
}
|
|
3261
3805
|
}
|
|
3262
3806
|
|
|
3807
|
+
// ══════════════════════════════════════════════════════════════
|
|
3808
|
+
// LICENSE MODAL
|
|
3809
|
+
// ══════════════════════════════════════════════════════════════
|
|
3810
|
+
function setupLicenseModal() {
|
|
3811
|
+
const btn = document.getElementById("btn-license");
|
|
3812
|
+
const modal = document.getElementById("license-modal");
|
|
3813
|
+
const input = document.getElementById("license-key-input");
|
|
3814
|
+
const activateBtn = document.getElementById("btn-license-activate");
|
|
3815
|
+
const deactivateBtn = document.getElementById("btn-license-deactivate");
|
|
3816
|
+
const cancelBtn = document.getElementById("btn-license-cancel");
|
|
3817
|
+
const statusEl = document.getElementById("license-modal-status");
|
|
3818
|
+
if (!btn || !modal) return;
|
|
3819
|
+
|
|
3820
|
+
function openModal() {
|
|
3821
|
+
modal.classList.remove("hidden");
|
|
3822
|
+
input.value = "";
|
|
3823
|
+
statusEl.textContent = "";
|
|
3824
|
+
statusEl.className = "";
|
|
3825
|
+
input.focus();
|
|
3826
|
+
}
|
|
3827
|
+
function closeModal() {
|
|
3828
|
+
modal.classList.add("hidden");
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
btn.addEventListener("click", openModal);
|
|
3832
|
+
cancelBtn.addEventListener("click", closeModal);
|
|
3833
|
+
modal.addEventListener("click", (e) => { if (e.target === modal) closeModal(); });
|
|
3834
|
+
|
|
3835
|
+
activateBtn.addEventListener("click", async () => {
|
|
3836
|
+
const key = input.value.trim();
|
|
3837
|
+
if (!key || !key.startsWith("SYKE-")) {
|
|
3838
|
+
statusEl.className = "error";
|
|
3839
|
+
statusEl.textContent = "Key must start with SYKE-";
|
|
3840
|
+
return;
|
|
3841
|
+
}
|
|
3842
|
+
statusEl.className = "loading";
|
|
3843
|
+
statusEl.textContent = "VALIDATING...";
|
|
3844
|
+
activateBtn.disabled = true;
|
|
3845
|
+
|
|
3846
|
+
try {
|
|
3847
|
+
const res = await fetch("/api/set-license-key", {
|
|
3848
|
+
method: "POST",
|
|
3849
|
+
headers: { "Content-Type": "application/json" },
|
|
3850
|
+
body: JSON.stringify({ key }),
|
|
3851
|
+
});
|
|
3852
|
+
const data = await res.json();
|
|
3853
|
+
if (data.success && data.plan === "pro") {
|
|
3854
|
+
statusEl.className = "success";
|
|
3855
|
+
statusEl.textContent = "PRO ACTIVATED";
|
|
3856
|
+
updateLicenseBadge("pro", data.expiresAt);
|
|
3857
|
+
updateLicenseButton("pro");
|
|
3858
|
+
setTimeout(closeModal, 1200);
|
|
3859
|
+
} else {
|
|
3860
|
+
statusEl.className = "error";
|
|
3861
|
+
statusEl.textContent = data.error || "Activation failed";
|
|
3862
|
+
}
|
|
3863
|
+
} catch (err) {
|
|
3864
|
+
statusEl.className = "error";
|
|
3865
|
+
statusEl.textContent = "Network error";
|
|
3866
|
+
}
|
|
3867
|
+
activateBtn.disabled = false;
|
|
3868
|
+
});
|
|
3869
|
+
|
|
3870
|
+
deactivateBtn.addEventListener("click", async () => {
|
|
3871
|
+
if (!confirm("Remove license key? Dashboard will switch to Free mode.")) return;
|
|
3872
|
+
statusEl.className = "loading";
|
|
3873
|
+
statusEl.textContent = "REMOVING...";
|
|
3874
|
+
|
|
3875
|
+
try {
|
|
3876
|
+
const res = await fetch("/api/set-license-key", {
|
|
3877
|
+
method: "POST",
|
|
3878
|
+
headers: { "Content-Type": "application/json" },
|
|
3879
|
+
body: JSON.stringify({ key: null }),
|
|
3880
|
+
});
|
|
3881
|
+
const data = await res.json();
|
|
3882
|
+
if (data.success) {
|
|
3883
|
+
statusEl.className = "success";
|
|
3884
|
+
statusEl.textContent = "KEY REMOVED";
|
|
3885
|
+
updateLicenseBadge("free", null);
|
|
3886
|
+
updateLicenseButton("free");
|
|
3887
|
+
setTimeout(closeModal, 800);
|
|
3888
|
+
}
|
|
3889
|
+
} catch {
|
|
3890
|
+
statusEl.className = "error";
|
|
3891
|
+
statusEl.textContent = "Failed to remove key";
|
|
3892
|
+
}
|
|
3893
|
+
});
|
|
3894
|
+
|
|
3895
|
+
input.addEventListener("keydown", (e) => {
|
|
3896
|
+
if (e.key === "Enter") activateBtn.click();
|
|
3897
|
+
if (e.key === "Escape") closeModal();
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
function updateLicenseButton(plan) {
|
|
3902
|
+
const btn = document.getElementById("btn-license");
|
|
3903
|
+
if (!btn) return;
|
|
3904
|
+
btn.textContent = plan === "pro" ? "LICENSED" : "LICENSE";
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3263
3907
|
function setupProjectModal() {
|
|
3264
3908
|
const openBtn = document.getElementById("btn-change-project");
|
|
3265
3909
|
const modal = document.getElementById("project-modal");
|