@syke1/mcp-server 1.3.19 → 1.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.
@@ -20,12 +20,378 @@ let heartbeatNodes = new Map(); // nodeId → { riskLevel, startTime, interval }
20
20
  let diffScrollAnim = null; // animation for diff scroll
21
21
  let knownNodeIds = new Set(); // track existing nodes for star-birth detection
22
22
  let birthAnimations = new Map(); // nodeId → { startTime, spawnPos, targetPos }
23
+ let searchActive = false; // true when search input has text
24
+ let _searchRAF = null; // RAF loop for search glow animation
23
25
 
24
- const LAYER_HEX = {
26
+ let LAYER_HEX = {
25
27
  FE: "#00d4ff", BE: "#c084fc", DB: "#ff6b35",
26
28
  API: "#00ffaa", CONFIG: "#ffd700", UTIL: "#ff69b4",
27
29
  };
28
30
 
31
+ // ═══════════════════════════════════════════
32
+ // SETTINGS SYSTEM
33
+ // ═══════════════════════════════════════════
34
+ const SETTINGS_DEFAULTS = {
35
+ nodes: {
36
+ sizeMin: 30,
37
+ sizeMultiplier: 3,
38
+ opacity: 1.0,
39
+ resolution: 16,
40
+ selectedColor: "#ffffff",
41
+ },
42
+ links: {
43
+ normalWidth: 1.5,
44
+ highlightWidth: 4.0,
45
+ opacity: 0.9,
46
+ curvatureBase: 0.12,
47
+ normalAlpha: 0.25,
48
+ highlightColor: "#ff2d55",
49
+ },
50
+ particles: {
51
+ normalCount: 6,
52
+ highlightCount: 14,
53
+ normalWidth: 2.0,
54
+ highlightWidth: 4.5,
55
+ normalSpeed: 0.006,
56
+ highlightSpeed: 0.004,
57
+ highlightColor: "#ffffff",
58
+ },
59
+ colors: {
60
+ FE: "#00d4ff",
61
+ BE: "#c084fc",
62
+ DB: "#ff6b35",
63
+ API: "#00ffaa",
64
+ CONFIG: "#ffd700",
65
+ UTIL: "#ff69b4",
66
+ },
67
+ arrows: {
68
+ length: 4,
69
+ position: 1,
70
+ },
71
+ scene: {
72
+ background: "#050a18",
73
+ ambientIntensity: 8,
74
+ pointIntensity: 3,
75
+ pointDistance: 5000,
76
+ fogDensity: 0.00012,
77
+ scanlineOpacity: 1.0,
78
+ },
79
+ camera: {
80
+ initialZ: 3500,
81
+ autoRotateSpeed: 0.0005,
82
+ autoRotateRadius: 1600,
83
+ resetDistance: 1600,
84
+ },
85
+ animation: {
86
+ birthDuration: 2500,
87
+ birthScale: 3,
88
+ spawnX: 5000,
89
+ spawnY: 4000,
90
+ spawnZ: -2000,
91
+ },
92
+ physics: {
93
+ alphaDecay: 0.008,
94
+ velocityDecay: 0.3,
95
+ chargeStrength: -800,
96
+ sameLayerDistance: 250,
97
+ crossLayerDistance: 900,
98
+ clusterStrength: 0.015,
99
+ },
100
+ };
101
+
102
+ function deepMerge(defaults, override) {
103
+ const result = {};
104
+ for (const key of Object.keys(defaults)) {
105
+ if (typeof defaults[key] === "object" && defaults[key] !== null && !Array.isArray(defaults[key])) {
106
+ result[key] = deepMerge(defaults[key], override?.[key] || {});
107
+ } else {
108
+ result[key] = override?.[key] !== undefined ? override[key] : defaults[key];
109
+ }
110
+ }
111
+ return result;
112
+ }
113
+
114
+ function loadSettings() {
115
+ try {
116
+ const stored = JSON.parse(localStorage.getItem("syke-dashboard-settings") || "{}");
117
+ return deepMerge(SETTINGS_DEFAULTS, stored);
118
+ } catch (e) {
119
+ console.warn("[SYKE] Failed to load settings, using defaults", e);
120
+ return deepMerge(SETTINGS_DEFAULTS, {});
121
+ }
122
+ }
123
+
124
+ const SETTINGS = loadSettings();
125
+
126
+ // Apply colors from settings on startup
127
+ Object.assign(LAYER_HEX, SETTINGS.colors);
128
+
129
+ function saveSettings() {
130
+ try {
131
+ localStorage.setItem("syke-dashboard-settings", JSON.stringify(SETTINGS));
132
+ } catch (e) {
133
+ console.warn("[SYKE] Failed to save settings", e);
134
+ }
135
+ }
136
+
137
+ function resetSettingsGroup(group) {
138
+ if (SETTINGS_DEFAULTS[group]) {
139
+ SETTINGS[group] = deepMerge(SETTINGS_DEFAULTS[group], {});
140
+ saveSettings();
141
+ }
142
+ }
143
+
144
+ function resetAllSettings() {
145
+ for (const group of Object.keys(SETTINGS_DEFAULTS)) {
146
+ SETTINGS[group] = deepMerge(SETTINGS_DEFAULTS[group], {});
147
+ }
148
+ Object.assign(LAYER_HEX, SETTINGS.colors);
149
+ saveSettings();
150
+ }
151
+
152
+ function applySettings(group) {
153
+ if (!Graph) return;
154
+ switch (group) {
155
+ case "nodes":
156
+ refreshGraph();
157
+ break;
158
+ case "links":
159
+ Graph.linkCurvature(link => SETTINGS.links.curvatureBase + (hc(getSrcId(link) + getTgtId(link)) % 20) * 0.01);
160
+ Graph.linkOpacity(SETTINGS.links.opacity);
161
+ refreshGraph();
162
+ break;
163
+ case "particles":
164
+ refreshGraph();
165
+ break;
166
+ case "colors":
167
+ Object.assign(LAYER_HEX, SETTINGS.colors);
168
+ if (graphData) {
169
+ const layerCounts = {};
170
+ graphData.nodes.forEach(n => { layerCounts[n.layer] = (layerCounts[n.layer] || 0) + 1; });
171
+ buildLegend(layerCounts);
172
+ }
173
+ refreshGraph();
174
+ break;
175
+ case "arrows":
176
+ Graph.linkDirectionalArrowLength(SETTINGS.arrows.length);
177
+ Graph.linkDirectionalArrowRelPos(SETTINGS.arrows.position);
178
+ break;
179
+ case "scene":
180
+ Graph.backgroundColor(SETTINGS.scene.background);
181
+ try {
182
+ const scene = Graph.scene();
183
+ if (scene) {
184
+ scene.children.forEach(c => {
185
+ if (c.isAmbientLight) c.intensity = SETTINGS.scene.ambientIntensity;
186
+ if (c.isPointLight) { c.intensity = SETTINGS.scene.pointIntensity; c.distance = SETTINGS.scene.pointDistance; }
187
+ });
188
+ if (scene.fog) scene.fog.density = SETTINGS.scene.fogDensity;
189
+ }
190
+ } catch(e) {}
191
+ const scanline = document.getElementById("scanline");
192
+ if (scanline) scanline.style.opacity = SETTINGS.scene.scanlineOpacity;
193
+ break;
194
+ case "physics":
195
+ Graph.d3AlphaDecay(SETTINGS.physics.alphaDecay);
196
+ Graph.d3VelocityDecay(SETTINGS.physics.velocityDecay);
197
+ try {
198
+ Graph.d3Force("charge").strength(SETTINGS.physics.chargeStrength);
199
+ Graph.d3Force("link")
200
+ .distance(l => srcLayer(l) === tgtLayer(l) ? SETTINGS.physics.sameLayerDistance : SETTINGS.physics.crossLayerDistance);
201
+ Graph.d3Force("cluster", clusterForce(SETTINGS.physics.clusterStrength));
202
+ } catch(e) {}
203
+ Graph.d3ReheatSimulation();
204
+ break;
205
+ case "camera":
206
+ // Camera values are read live in autoRotate loop, no action needed
207
+ break;
208
+ case "animation":
209
+ // Animation values are read when new nodes appear, no action needed
210
+ break;
211
+ }
212
+ }
213
+
214
+ function setupSettings() {
215
+ const CTRL_MAP = [
216
+ // [group, key, selector, type]
217
+ ["nodes", "sizeMin", "#set-node-sizeMin", "range"],
218
+ ["nodes", "sizeMultiplier", "#set-node-sizeMultiplier", "range"],
219
+ ["nodes", "opacity", "#set-node-opacity", "range"],
220
+ ["nodes", "resolution", "#set-node-resolution", "range"],
221
+ ["nodes", "selectedColor", "#set-node-selectedColor", "color"],
222
+ ["links", "normalWidth", "#set-link-normalWidth", "range"],
223
+ ["links", "highlightWidth", "#set-link-highlightWidth", "range"],
224
+ ["links", "opacity", "#set-link-opacity", "range"],
225
+ ["links", "curvatureBase", "#set-link-curvatureBase", "range"],
226
+ ["links", "normalAlpha", "#set-link-normalAlpha", "range"],
227
+ ["links", "highlightColor", "#set-link-highlightColor", "color"],
228
+ ["particles", "normalCount", "#set-part-normalCount", "range"],
229
+ ["particles", "highlightCount", "#set-part-highlightCount", "range"],
230
+ ["particles", "normalWidth", "#set-part-normalWidth", "range"],
231
+ ["particles", "highlightWidth", "#set-part-highlightWidth", "range"],
232
+ ["particles", "normalSpeed", "#set-part-normalSpeed", "range"],
233
+ ["particles", "highlightSpeed", "#set-part-highlightSpeed", "range"],
234
+ ["particles", "highlightColor", "#set-part-highlightColor", "color"],
235
+ ["colors", "FE", "#set-color-FE", "color"],
236
+ ["colors", "BE", "#set-color-BE", "color"],
237
+ ["colors", "DB", "#set-color-DB", "color"],
238
+ ["colors", "API", "#set-color-API", "color"],
239
+ ["colors", "CONFIG", "#set-color-CONFIG", "color"],
240
+ ["colors", "UTIL", "#set-color-UTIL", "color"],
241
+ ["arrows", "length", "#set-arrow-length", "range"],
242
+ ["arrows", "position", "#set-arrow-position", "range"],
243
+ ["scene", "background", "#set-scene-background", "color"],
244
+ ["scene", "ambientIntensity", "#set-scene-ambientIntensity", "range"],
245
+ ["scene", "pointIntensity", "#set-scene-pointIntensity", "range"],
246
+ ["scene", "pointDistance", "#set-scene-pointDistance", "range"],
247
+ ["scene", "fogDensity", "#set-scene-fogDensity", "range"],
248
+ ["scene", "scanlineOpacity", "#set-scene-scanlineOpacity", "range"],
249
+ ["camera", "initialZ", "#set-cam-initialZ", "range"],
250
+ ["camera", "autoRotateSpeed", "#set-cam-autoRotateSpeed", "range"],
251
+ ["camera", "autoRotateRadius", "#set-cam-autoRotateRadius", "range"],
252
+ ["camera", "resetDistance", "#set-cam-resetDistance", "range"],
253
+ ["physics", "alphaDecay", "#set-phys-alphaDecay", "range"],
254
+ ["physics", "velocityDecay", "#set-phys-velocityDecay", "range"],
255
+ ["physics", "chargeStrength", "#set-phys-chargeStrength", "range"],
256
+ ["physics", "sameLayerDistance", "#set-phys-sameLayerDistance", "range"],
257
+ ["physics", "crossLayerDistance", "#set-phys-crossLayerDistance", "range"],
258
+ ["physics", "clusterStrength", "#set-phys-clusterStrength", "range"],
259
+ ["animation", "birthDuration", "#set-anim-birthDuration", "range"],
260
+ ["animation", "birthScale", "#set-anim-birthScale", "range"],
261
+ ["animation", "spawnX", "#set-anim-spawnX", "range"],
262
+ ["animation", "spawnY", "#set-anim-spawnY", "range"],
263
+ ["animation", "spawnZ", "#set-anim-spawnZ", "range"],
264
+ ];
265
+
266
+ // Init values and bind events
267
+ for (const [group, key, selector, type] of CTRL_MAP) {
268
+ const el = document.querySelector(selector);
269
+ if (!el) continue;
270
+ const valEl = el.parentElement?.querySelector(".set-val");
271
+ el.value = SETTINGS[group][key];
272
+ if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
273
+
274
+ el.addEventListener("input", () => {
275
+ const v = type === "color" ? el.value : parseFloat(el.value);
276
+ SETTINGS[group][key] = v;
277
+ if (valEl) valEl.textContent = formatSetVal(v, type);
278
+ applySettings(group);
279
+ saveSettings();
280
+ });
281
+ }
282
+
283
+ // Collapsible sections
284
+ document.querySelectorAll(".set-section-hdr").forEach(hdr => {
285
+ hdr.addEventListener("click", () => {
286
+ const body = hdr.nextElementSibling;
287
+ const arrow = hdr.querySelector(".set-arrow");
288
+ if (body) body.classList.toggle("collapsed");
289
+ if (arrow) arrow.classList.toggle("open");
290
+ });
291
+ });
292
+
293
+ // Reset per group
294
+ document.querySelectorAll(".set-rst-btn").forEach(btn => {
295
+ btn.addEventListener("click", (e) => {
296
+ e.stopPropagation();
297
+ const group = btn.dataset.group;
298
+ resetSettingsGroup(group);
299
+ applySettings(group);
300
+ // Refresh UI inputs
301
+ for (const [g, key, selector, type] of CTRL_MAP) {
302
+ if (g !== group) continue;
303
+ const el = document.querySelector(selector);
304
+ if (!el) continue;
305
+ el.value = SETTINGS[group][key];
306
+ const valEl = el.parentElement?.querySelector(".set-val");
307
+ if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
308
+ }
309
+ });
310
+ });
311
+
312
+ // Reset all
313
+ const resetAllBtn = document.getElementById("set-reset-all");
314
+ if (resetAllBtn) {
315
+ resetAllBtn.addEventListener("click", () => {
316
+ resetAllSettings();
317
+ for (const group of Object.keys(SETTINGS_DEFAULTS)) applySettings(group);
318
+ // Refresh all UI inputs
319
+ for (const [group, key, selector, type] of CTRL_MAP) {
320
+ const el = document.querySelector(selector);
321
+ if (!el) continue;
322
+ el.value = SETTINGS[group][key];
323
+ const valEl = el.parentElement?.querySelector(".set-val");
324
+ if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
325
+ }
326
+ });
327
+ }
328
+
329
+ // Export
330
+ const exportBtn = document.getElementById("set-export");
331
+ if (exportBtn) {
332
+ exportBtn.addEventListener("click", () => {
333
+ const blob = new Blob([JSON.stringify(SETTINGS, null, 2)], { type: "application/json" });
334
+ const url = URL.createObjectURL(blob);
335
+ const a = document.createElement("a");
336
+ a.href = url;
337
+ a.download = "syke-settings.json";
338
+ a.click();
339
+ URL.revokeObjectURL(url);
340
+ });
341
+ }
342
+
343
+ // Import
344
+ const importBtn = document.getElementById("set-import");
345
+ if (importBtn) {
346
+ importBtn.addEventListener("click", () => {
347
+ const input = document.createElement("input");
348
+ input.type = "file";
349
+ input.accept = ".json";
350
+ input.addEventListener("change", () => {
351
+ const file = input.files[0];
352
+ if (!file) return;
353
+ const reader = new FileReader();
354
+ reader.onload = () => {
355
+ try {
356
+ const imported = JSON.parse(reader.result);
357
+ const merged = deepMerge(SETTINGS_DEFAULTS, imported);
358
+ for (const group of Object.keys(merged)) {
359
+ SETTINGS[group] = merged[group];
360
+ }
361
+ Object.assign(LAYER_HEX, SETTINGS.colors);
362
+ saveSettings();
363
+ for (const group of Object.keys(SETTINGS_DEFAULTS)) applySettings(group);
364
+ // Refresh all UI inputs
365
+ for (const [group, key, selector, type] of CTRL_MAP) {
366
+ const el = document.querySelector(selector);
367
+ if (!el) continue;
368
+ el.value = SETTINGS[group][key];
369
+ const valEl = el.parentElement?.querySelector(".set-val");
370
+ if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
371
+ }
372
+ } catch(e) {
373
+ console.error("[SYKE] Import failed:", e);
374
+ }
375
+ };
376
+ reader.readAsText(file);
377
+ });
378
+ input.click();
379
+ });
380
+ }
381
+
382
+ // Apply scanline opacity on startup
383
+ const scanline = document.getElementById("scanline");
384
+ if (scanline) scanline.style.opacity = SETTINGS.scene.scanlineOpacity;
385
+ }
386
+
387
+ function formatSetVal(v, type) {
388
+ if (type === "color") return v;
389
+ if (Number.isInteger(v)) return v.toString();
390
+ if (Math.abs(v) < 0.01) return v.toFixed(5);
391
+ if (Math.abs(v) < 1) return v.toFixed(3);
392
+ return v.toFixed(1);
393
+ }
394
+
29
395
  const LAYER_KEYS = ["FE", "BE", "DB", "API", "CONFIG", "UTIL"];
30
396
 
31
397
  const LAYER_CENTERS = {
@@ -49,6 +415,7 @@ document.addEventListener("DOMContentLoaded", async () => {
49
415
  setupKeyboardShortcuts();
50
416
  setupContextMenu();
51
417
  setupTabs();
418
+ setupSettings();
52
419
  setupProjectModal();
53
420
  initSSE();
54
421
  startHealthCheck();
@@ -113,7 +480,7 @@ async function loadGraph() {
113
480
  const isReload = Graph !== null;
114
481
 
115
482
  // Spawn point for new nodes (top-right corner of 3D space)
116
- const SPAWN = { x: 5000, y: 4000, z: -2000 };
483
+ const SPAWN = { x: SETTINGS.animation.spawnX, y: SETTINGS.animation.spawnY, z: SETTINGS.animation.spawnZ };
117
484
 
118
485
  // Preserve existing node positions on reload
119
486
  const currentPositions = {};
@@ -143,7 +510,7 @@ async function loadGraph() {
143
510
  startTime: Date.now(),
144
511
  spawnPos: { ...SPAWN },
145
512
  targetPos: { ...targetPos },
146
- duration: 2500, // 2.5s flight time
513
+ duration: SETTINGS.animation.birthDuration,
147
514
  });
148
515
  }
149
516
 
@@ -209,7 +576,7 @@ async function loadGraph() {
209
576
  // Phase 3: Zoom out to overview (at 3.5s, 1.5s transition)
210
577
  setTimeout(() => {
211
578
  Graph.cameraPosition(
212
- { x: 0, y: 0, z: 3500 },
579
+ { x: 0, y: 0, z: SETTINGS.camera.initialZ },
213
580
  { x: 0, y: 0, z: 0 },
214
581
  1500
215
582
  );
@@ -231,13 +598,19 @@ async function loadGraph() {
231
598
  .width(window.innerWidth - 380)
232
599
  .height(window.innerHeight - 100)
233
600
  .graphData(graphData)
234
- .backgroundColor("#050a18")
601
+ .backgroundColor(SETTINGS.scene.background)
235
602
  .showNavInfo(false)
236
603
 
237
604
  .nodeColor(node => getNodeColor(node))
238
605
  .nodeVal(node => {
239
606
  if (!isNodeVisible(node)) return 0.001;
240
- const base = Math.max(30, Math.sqrt(node.lineCount) * 3);
607
+ const base = Math.max(SETTINGS.nodes.sizeMin, Math.sqrt(node.lineCount) * SETTINGS.nodes.sizeMultiplier);
608
+ // Search mode: shrink non-matches, boost matches
609
+ if (searchActive) {
610
+ if (!highlightNodes.has(node.id)) return base * 0.3;
611
+ const pulse = 0.9 + 0.1 * Math.sin(Date.now() / 400);
612
+ return base * 1.4 * pulse;
613
+ }
241
614
  const hb = heartbeatNodes.get(node.id);
242
615
  if (hb) {
243
616
  const elapsed = Date.now() - hb.startTime;
@@ -255,13 +628,13 @@ async function loadGraph() {
255
628
  if (birth) {
256
629
  const t = Math.min(1, (Date.now() - birth.startTime) / birth.duration);
257
630
  if (t >= 1) birthAnimations.delete(node.id);
258
- const scale = 1 + (3 - 1) * (1 - t) * (1 - t); // ease-out: 3x → 1x
631
+ const scale = 1 + (SETTINGS.animation.birthScale - 1) * (1 - t) * (1 - t); // ease-out: Nx → 1x
259
632
  return base * scale;
260
633
  }
261
634
  return base;
262
635
  })
263
- .nodeOpacity(1.0)
264
- .nodeResolution(16)
636
+ .nodeOpacity(SETTINGS.nodes.opacity)
637
+ .nodeResolution(SETTINGS.nodes.resolution)
265
638
  .nodeVisibility(node => isNodeVisible(node))
266
639
  .nodeLabel(node => {
267
640
  const c = LAYER_HEX[node.layer] || "#ccc";
@@ -272,34 +645,36 @@ async function loadGraph() {
272
645
  })
273
646
 
274
647
  .linkColor(link => getLinkColor(link))
275
- .linkWidth(link => highlightLinks.has(link) ? 4.0 : 1.5)
276
- .linkOpacity(0.9)
648
+ .linkWidth(link => highlightLinks.has(link) ? SETTINGS.links.highlightWidth : SETTINGS.links.normalWidth)
649
+ .linkOpacity(SETTINGS.links.opacity)
277
650
  .linkVisibility(link => isLinkVisible(link))
278
- .linkCurvature(link => 0.12 + (hc(getSrcId(link) + getTgtId(link)) % 20) * 0.01)
651
+ .linkCurvature(link => SETTINGS.links.curvatureBase + (hc(getSrcId(link) + getTgtId(link)) % 20) * 0.01)
279
652
  .linkCurveRotation(link => (hc(getTgtId(link) + getSrcId(link)) % 628) / 100)
280
653
 
281
654
  .linkDirectionalParticles(link => {
282
- if (highlightLinks.has(link)) return 14;
655
+ if (searchActive) return highlightLinks.has(link) ? SETTINGS.particles.highlightCount : 0;
656
+ if (highlightLinks.has(link)) return SETTINGS.particles.highlightCount;
283
657
  const isActive = modifyingNodes.size > 0 || heartbeatNodes.size > 0;
284
- return isActive ? 4 : 6;
658
+ return isActive ? Math.round(SETTINGS.particles.normalCount * 0.67) : SETTINGS.particles.normalCount;
285
659
  })
286
660
  .linkDirectionalParticleWidth(link => {
287
- if (highlightLinks.has(link)) return 4.5;
661
+ if (highlightLinks.has(link)) return SETTINGS.particles.highlightWidth;
288
662
  const isActive = modifyingNodes.size > 0 || heartbeatNodes.size > 0;
289
- return isActive ? 1.5 : 2.0;
663
+ return isActive ? SETTINGS.particles.normalWidth * 0.75 : SETTINGS.particles.normalWidth;
290
664
  })
291
- .linkDirectionalParticleSpeed(link => highlightLinks.has(link) ? 0.004 : 0.006)
665
+ .linkDirectionalParticleSpeed(link => highlightLinks.has(link) ? SETTINGS.particles.highlightSpeed : SETTINGS.particles.normalSpeed)
292
666
  .linkDirectionalParticleColor(link => {
293
- if (highlightLinks.has(link)) return "#ffffff";
667
+ if (searchActive && highlightLinks.has(link)) return LAYER_HEX[srcLayer(link)] || "#00d4ff";
668
+ if (highlightLinks.has(link)) return SETTINGS.particles.highlightColor;
294
669
  const isActive = modifyingNodes.size > 0 || heartbeatNodes.size > 0;
295
670
  if (isActive) return "rgba(150,180,220,0.6)";
296
671
  return LAYER_HEX[srcLayer(link)] || "#ff69b4";
297
672
  })
298
673
 
299
- .linkDirectionalArrowLength(4)
300
- .linkDirectionalArrowRelPos(1)
674
+ .linkDirectionalArrowLength(SETTINGS.arrows.length)
675
+ .linkDirectionalArrowRelPos(SETTINGS.arrows.position)
301
676
  .linkDirectionalArrowColor(link => {
302
- if (highlightLinks.has(link)) return "#ff2d55";
677
+ if (highlightLinks.has(link)) return SETTINGS.links.highlightColor;
303
678
  return rgba(LAYER_HEX[srcLayer(link)] || "#ff69b4", 0.4);
304
679
  })
305
680
 
@@ -345,27 +720,28 @@ async function loadGraph() {
345
720
  }
346
721
  })
347
722
 
348
- .d3AlphaDecay(0.008)
349
- .d3VelocityDecay(0.3)
723
+ .d3AlphaDecay(SETTINGS.physics.alphaDecay)
724
+ .d3VelocityDecay(SETTINGS.physics.velocityDecay)
350
725
  .warmupTicks(300)
351
726
  .cooldownTicks(800)
352
727
  .enablePointerInteraction(true);
353
728
 
354
- Graph.d3Force("cluster", clusterForce(0.015));
355
- Graph.d3Force("charge").strength(-800);
729
+ Graph.d3Force("cluster", clusterForce(SETTINGS.physics.clusterStrength));
730
+ Graph.d3Force("charge").strength(SETTINGS.physics.chargeStrength);
356
731
  Graph.d3Force("link")
357
- .distance(l => srcLayer(l) === tgtLayer(l) ? 250 : 900)
732
+ .distance(l => srcLayer(l) === tgtLayer(l) ? SETTINGS.physics.sameLayerDistance : SETTINGS.physics.crossLayerDistance)
358
733
  .strength(l => srcLayer(l) === tgtLayer(l) ? 0.2 : 0.05);
359
734
 
360
- Graph.cameraPosition({ x: 0, y: 0, z: 3500 });
735
+ Graph.cameraPosition({ x: 0, y: 0, z: SETTINGS.camera.initialZ });
361
736
 
362
737
  setTimeout(() => {
363
738
  try {
364
739
  const scene = Graph.scene();
365
740
  if (!scene) return;
366
- scene.add(new THREE.AmbientLight(0xffffff, 8));
367
- scene.add(new THREE.PointLight(0xffffff, 3, 5000));
368
- scene.fog = new THREE.FogExp2(0x050a18, 0.00012);
741
+ scene.add(new THREE.AmbientLight(0xffffff, SETTINGS.scene.ambientIntensity));
742
+ const ptLight = new THREE.PointLight(0xffffff, SETTINGS.scene.pointIntensity, SETTINGS.scene.pointDistance);
743
+ scene.add(ptLight);
744
+ scene.fog = new THREE.FogExp2(parseInt(SETTINGS.scene.background.replace("#",""), 16), SETTINGS.scene.fogDensity);
369
745
  console.log("[SYKE] Scene ready");
370
746
  } catch(e) { console.warn(e); }
371
747
  }, 500);
@@ -528,13 +904,24 @@ function getNodeColor(node) {
528
904
  const pulse = 0.5 + 0.5 * Math.sin(t);
529
905
  return `rgb(255,${Math.round(180 + pulse * 75)},${Math.round(50 + pulse * 50)})`;
530
906
  }
531
- if (node.id === selectedNodeId) return "#ffffff";
907
+ if (node.id === selectedNodeId) return SETTINGS.nodes.selectedColor;
532
908
  const base = LAYER_HEX[node.layer] || "#ff69b4";
533
909
  if (highlightNodes.size > 0 && !highlightNodes.has(node.id)) {
534
- // When actively modifying → dim harder so protagonists stand out
910
+ if (searchActive) {
911
+ // Search mode: near-invisible ghost nodes
912
+ return dimHex(base, 0.06);
913
+ }
535
914
  const isActive = modifyingNodes.size > 0 || heartbeatNodes.size > 0;
536
915
  return dimHex(base, isActive ? 0.12 : 0.35);
537
916
  }
917
+ // Search match: pulsing bright glow
918
+ if (searchActive && highlightNodes.has(node.id)) {
919
+ const pulse = 0.85 + 0.15 * Math.sin(Date.now() / 300);
920
+ const r = Math.min(255, Math.round(parseInt(base.slice(1,3),16) * pulse + 40));
921
+ const g = Math.min(255, Math.round(parseInt(base.slice(3,5),16) * pulse + 40));
922
+ const b = Math.min(255, Math.round(parseInt(base.slice(5,7),16) * pulse + 40));
923
+ return `rgb(${r},${g},${b})`;
924
+ }
538
925
  return base;
539
926
  }
540
927
 
@@ -599,12 +986,17 @@ function dimHex(hex, factor) {
599
986
  }
600
987
 
601
988
  function getLinkColor(link) {
602
- if (highlightLinks.has(link)) return "#ff2d55";
989
+ if (searchActive && highlightLinks.has(link)) {
990
+ // Search mode: matched links glow with source layer color
991
+ return LAYER_HEX[srcLayer(link)] || "#00d4ff";
992
+ }
993
+ if (highlightLinks.has(link)) return SETTINGS.links.highlightColor;
603
994
  if (highlightNodes.size > 0 && !highlightLinks.has(link)) {
995
+ if (searchActive) return "rgba(30,40,60,0.02)"; // near-invisible
604
996
  const isActive = modifyingNodes.size > 0 || heartbeatNodes.size > 0;
605
997
  return isActive ? "rgba(60,70,90,0.03)" : "rgba(100,120,150,0.06)";
606
998
  }
607
- return rgba(LAYER_HEX[srcLayer(link)] || "#ff69b4", 0.25);
999
+ return rgba(LAYER_HEX[srcLayer(link)] || "#ff69b4", SETTINGS.links.normalAlpha);
608
1000
  }
609
1001
 
610
1002
  // ═══════════════════════════════════════════
@@ -689,11 +1081,11 @@ function buildLegend(counts) {
689
1081
  let rotAngle = 0, rotRAF = null;
690
1082
  function startAutoRotate() {
691
1083
  if (!autoRotate || !Graph) return;
692
- rotAngle += 0.0005;
1084
+ rotAngle += SETTINGS.camera.autoRotateSpeed;
693
1085
  Graph.cameraPosition({
694
- x: 1600 * Math.sin(rotAngle),
1086
+ x: SETTINGS.camera.autoRotateRadius * Math.sin(rotAngle),
695
1087
  y: 200 + Math.sin(rotAngle * 0.3) * 100,
696
- z: 1600 * Math.cos(rotAngle),
1088
+ z: SETTINGS.camera.autoRotateRadius * Math.cos(rotAngle),
697
1089
  });
698
1090
  rotRAF = requestAnimationFrame(startAutoRotate);
699
1091
  }
@@ -783,7 +1175,12 @@ async function showImpact(fileId, nd) {
783
1175
  document.getElementById("impact-content").innerHTML = '<div class="loading"><div class="spinner"></div>TRACING...</div>';
784
1176
 
785
1177
  try {
786
- const res = await fetch("/api/impact/" + fileId);
1178
+ const res = await fetch("/api/impact/" + fileId.split("/").map(encodeURIComponent).join("/"));
1179
+ const ct = res.headers.get("content-type") || "";
1180
+ if (!ct.includes("application/json")) {
1181
+ document.getElementById("impact-content").innerHTML = `<p class="placeholder">ERROR: Server returned non-JSON (${res.status})</p>`;
1182
+ return;
1183
+ }
787
1184
  const impact = await res.json();
788
1185
  if (!res.ok) { document.getElementById("impact-content").innerHTML = `<p class="placeholder">ERROR: ${impact.error}</p>`; return; }
789
1186
 
@@ -876,7 +1273,12 @@ async function loadCodePreview(fileId) {
876
1273
  const el = document.getElementById("code-content");
877
1274
  el.innerHTML = '<div class="loading"><div class="spinner"></div>LOADING...</div>';
878
1275
  try {
879
- const res = await fetch("/api/file-content/" + fileId);
1276
+ const res = await fetch("/api/file-content/" + fileId.split("/").map(encodeURIComponent).join("/"));
1277
+ const ct = res.headers.get("content-type") || "";
1278
+ if (!ct.includes("application/json")) {
1279
+ el.innerHTML = `<p class="placeholder">ERROR: Server returned non-JSON (${res.status}). File: ${fileId}</p>`;
1280
+ return;
1281
+ }
880
1282
  const data = await res.json();
881
1283
  if (!res.ok) { el.innerHTML = `<p class="placeholder">ERROR: ${data.error}</p>`; return; }
882
1284
 
@@ -901,7 +1303,12 @@ async function loadSimulation(fileId) {
901
1303
  const el = document.getElementById("sim-content");
902
1304
  el.innerHTML = '<div class="loading"><div class="spinner"></div>SIMULATING...</div>';
903
1305
  try {
904
- const res = await fetch("/api/simulate-delete/" + fileId);
1306
+ const res = await fetch("/api/simulate-delete/" + fileId.split("/").map(encodeURIComponent).join("/"));
1307
+ const ct = res.headers.get("content-type") || "";
1308
+ if (!ct.includes("application/json")) {
1309
+ el.innerHTML = `<p class="placeholder">ERROR: Server returned non-JSON (${res.status})</p>`;
1310
+ return;
1311
+ }
905
1312
  const data = await res.json();
906
1313
  if (!res.ok) { el.innerHTML = `<p class="placeholder">ERROR: ${data.error}</p>`; return; }
907
1314
 
@@ -1495,7 +1902,7 @@ function resetView() {
1495
1902
  if (pathMode) exitPathMode();
1496
1903
  stopCodeCrawl();
1497
1904
  refreshGraph();
1498
- Graph.cameraPosition({ x:0,y:0,z:1600 }, { x:0,y:0,z:0 }, 1000);
1905
+ Graph.cameraPosition({ x:0,y:0,z:SETTINGS.camera.resetDistance }, { x:0,y:0,z:0 }, 1000);
1499
1906
  }
1500
1907
 
1501
1908
  function toggleAutoRotate() {
@@ -1548,22 +1955,67 @@ function setupEventListeners() {
1548
1955
  document.getElementById("btn-cancel-path").addEventListener("click", exitPathMode);
1549
1956
 
1550
1957
  document.getElementById("search-input").addEventListener("input", e => {
1551
- const q = e.target.value.toLowerCase();
1958
+ const q = e.target.value.toLowerCase().trim();
1552
1959
  highlightNodes.clear(); highlightLinks.clear(); selectedNodeId = null;
1553
- if (q) {
1554
- graphData.nodes.forEach(n => {
1555
- if (n.fullPath.toLowerCase().includes(q) || n.layer.toLowerCase() === q || n.label.toLowerCase().includes(q)) highlightNodes.add(n.id);
1960
+
1961
+ if (!q) {
1962
+ // ── Clear search: restore full view ──
1963
+ searchActive = false;
1964
+ if (_searchRAF) { cancelAnimationFrame(_searchRAF); _searchRAF = null; }
1965
+ refreshGraph();
1966
+ // Smooth zoom-out to overview
1967
+ if (Graph) {
1968
+ Graph.cameraPosition(
1969
+ { x: 0, y: 0, z: SETTINGS.camera.resetDistance },
1970
+ { x: 0, y: 0, z: 0 }, 800
1971
+ );
1972
+ }
1973
+ return;
1974
+ }
1975
+
1976
+ // ── Active search: find matching nodes ──
1977
+ searchActive = true;
1978
+ graphData.nodes.forEach(n => {
1979
+ if (n.fullPath.toLowerCase().includes(q) || n.layer.toLowerCase() === q || n.label.toLowerCase().includes(q)) {
1980
+ highlightNodes.add(n.id);
1981
+ }
1982
+ });
1983
+
1984
+ // Also highlight links between matched nodes
1985
+ if (highlightNodes.size > 0) {
1986
+ graphData.links.forEach(l => {
1987
+ const sid = getSrcId(l), tid = getTgtId(l);
1988
+ if (highlightNodes.has(sid) || highlightNodes.has(tid)) highlightLinks.add(l);
1556
1989
  });
1557
- if (highlightNodes.size > 0) {
1558
- const f = graphData.nodes.find(n => n.id === highlightNodes.values().next().value);
1559
- if (f && f.x != null) {
1560
- stopUser();
1561
- const d = Math.max(1, Math.hypot(f.x,f.y||0,f.z||0));
1562
- Graph.cameraPosition({x:f.x+200*f.x/d,y:(f.y||0)+200*(f.y||1)/d,z:(f.z||0)+200*(f.z||1)/d},{x:f.x,y:f.y||0,z:f.z||0},600);
1990
+
1991
+ // Camera: frame all matched nodes (centroid)
1992
+ let cx = 0, cy = 0, cz = 0, cnt = 0;
1993
+ graphData.nodes.forEach(n => {
1994
+ if (highlightNodes.has(n.id) && n.x != null) {
1995
+ cx += n.x; cy += (n.y || 0); cz += (n.z || 0); cnt++;
1563
1996
  }
1997
+ });
1998
+ if (cnt > 0) {
1999
+ cx /= cnt; cy /= cnt; cz /= cnt;
2000
+ stopUser();
2001
+ const dist = cnt === 1 ? 600 : Math.min(2500, 800 + cnt * 80);
2002
+ Graph.cameraPosition(
2003
+ { x: cx + dist * 0.3, y: cy + dist * 0.2, z: cz + dist },
2004
+ { x: cx, y: cy, z: cz }, 600
2005
+ );
1564
2006
  }
1565
2007
  }
2008
+
1566
2009
  refreshGraph();
2010
+ // Start glow animation loop for search matches
2011
+ if (!_searchRAF && highlightNodes.size > 0) {
2012
+ function searchGlow() {
2013
+ if (!searchActive) { _searchRAF = null; return; }
2014
+ if (Graph) Graph.nodeColor(Graph.nodeColor());
2015
+ _searchRAF = requestAnimationFrame(searchGlow);
2016
+ }
2017
+ _searchRAF = requestAnimationFrame(searchGlow);
2018
+ }
1567
2019
  });
1568
2020
 
1569
2021
  document.getElementById("btn-toggle-hub").addEventListener("click", () => {
@@ -2587,82 +3039,30 @@ function setupResizeHandle() {
2587
3039
  // ═══════════════════════════════════════════
2588
3040
  // PROJECT SELECTOR
2589
3041
  // ═══════════════════════════════════════════
2590
- let licenseTimerInterval = null;
2591
-
2592
3042
  function updateLicenseBadge(plan, expiresAt) {
2593
3043
  const badge = document.getElementById("license-badge");
2594
3044
  if (!badge) return;
2595
3045
 
2596
- const planEl = badge.querySelector(".license-plan");
2597
- const timerEl = badge.querySelector(".license-timer");
2598
-
2599
- // Clear previous timer
2600
- if (licenseTimerInterval) {
2601
- clearInterval(licenseTimerInterval);
2602
- licenseTimerInterval = null;
2603
- }
2604
-
2605
3046
  badge.className = "license-badge";
2606
3047
 
2607
3048
  if (plan === "pro" && expiresAt) {
2608
- const expiry = new Date(expiresAt);
2609
- const now = new Date();
2610
- const diffMs = expiry - now;
2611
-
3049
+ const diffMs = new Date(expiresAt) - new Date();
2612
3050
  if (diffMs <= 0) {
2613
- // Expired
2614
3051
  badge.classList.add("expired");
2615
- planEl.textContent = "EXPIRED";
2616
- timerEl.textContent = "";
3052
+ badge.textContent = "EXPIRED";
3053
+ } else if (Math.ceil(diffMs / (1000 * 60 * 60 * 24)) <= 14) {
3054
+ badge.classList.add("trial");
3055
+ badge.textContent = "TRIAL";
2617
3056
  } else {
2618
- const totalDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
2619
-
2620
- if (totalDays <= 3) {
2621
- // Trial or near-expiry
2622
- badge.classList.add("trial-urgent");
2623
- planEl.textContent = "TRIAL";
2624
- } else if (totalDays <= 14) {
2625
- badge.classList.add("trial");
2626
- planEl.textContent = "TRIAL";
2627
- } else {
2628
- badge.classList.add("pro");
2629
- planEl.textContent = "PRO";
2630
- }
2631
-
2632
- // Live countdown
2633
- function tick() {
2634
- const remaining = new Date(expiresAt) - new Date();
2635
- if (remaining <= 0) {
2636
- timerEl.textContent = "EXPIRED";
2637
- badge.className = "license-badge expired";
2638
- planEl.textContent = "EXPIRED";
2639
- clearInterval(licenseTimerInterval);
2640
- return;
2641
- }
2642
- const d = Math.floor(remaining / (1000 * 60 * 60 * 24));
2643
- const h = Math.floor((remaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
2644
- const m = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
2645
- const s = Math.floor((remaining % (1000 * 60)) / 1000);
2646
-
2647
- if (d > 0) {
2648
- timerEl.textContent = `D-${d} ${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")}`;
2649
- } else {
2650
- timerEl.textContent = `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")}`;
2651
- }
2652
- }
2653
- tick();
2654
- licenseTimerInterval = setInterval(tick, 1000);
3057
+ badge.classList.add("pro");
3058
+ badge.textContent = "PRO";
2655
3059
  }
2656
3060
  } else if (plan === "pro") {
2657
- // Pro without expiry (permanent or subscription)
2658
3061
  badge.classList.add("pro");
2659
- planEl.textContent = "PRO";
2660
- timerEl.textContent = "";
3062
+ badge.textContent = "PRO";
2661
3063
  } else {
2662
- // Free
2663
3064
  badge.classList.add("free");
2664
- planEl.textContent = "FREE";
2665
- timerEl.textContent = "";
3065
+ badge.textContent = "FREE";
2666
3066
  }
2667
3067
  }
2668
3068
 
@@ -2731,7 +3131,7 @@ async function updateBottomBar() {
2731
3131
  const el = document.getElementById("bottom-info");
2732
3132
  if (!el) return;
2733
3133
  try {
2734
- const r = await fetch("https://registry.npmjs.org/@syke1/mcp-server");
3134
+ const r = await fetch("https://registry.npmjs.org/@syke1/mcp-server", { cache: "no-store" });
2735
3135
  const data = await r.json();
2736
3136
  const version = data["dist-tags"]?.latest || "?";
2737
3137
  const publishDate = data.time?.[version]?.slice(0, 10) || "?";
@@ -19,15 +19,12 @@
19
19
  <span class="logo-text">SYKE</span>
20
20
  <span class="logo-sub">CODE IMPACT RADAR</span>
21
21
  <span id="sse-status" class="sse-indicator offline">OFFLINE</span>
22
+ <span id="license-badge" class="license-badge free">FREE</span>
22
23
  </div>
23
24
  <div class="project-selector">
24
25
  <span id="current-project" class="project-path">Loading...</span>
25
26
  <button id="btn-change-project" class="top-btn" title="Switch project">OPEN</button>
26
27
  </div>
27
- <div id="license-badge" class="license-badge free">
28
- <span class="license-plan">FREE</span>
29
- <span class="license-timer"></span>
30
- </div>
31
28
  <div class="top-controls">
32
29
  <button id="btn-cycles" class="top-btn" title="Detect circular dependencies">CYCLES</button>
33
30
  <button id="btn-stats" class="top-btn" title="Toggle statistics panel">STATS</button>
@@ -134,6 +131,7 @@
134
131
  <button class="panel-tab" data-tab="code">CODE</button>
135
132
  <button class="panel-tab" data-tab="simulate">SIM</button>
136
133
  <button class="panel-tab" data-tab="live">LIVE</button>
134
+ <button class="panel-tab" data-tab="settings">SET</button>
137
135
  </div>
138
136
 
139
137
  <!-- Tab: Info (default) -->
@@ -192,6 +190,105 @@
192
190
  </div>
193
191
  </section>
194
192
  </div>
193
+
194
+ <!-- Tab: Settings -->
195
+ <div id="tab-settings" class="tab-content">
196
+ <div class="set-panel">
197
+ <!-- Top action bar -->
198
+ <div class="set-actions">
199
+ <button id="set-reset-all" class="set-action-btn set-danger">RESET ALL</button>
200
+ <button id="set-export" class="set-action-btn">EXPORT</button>
201
+ <button id="set-import" class="set-action-btn">IMPORT</button>
202
+ </div>
203
+
204
+ <div class="set-scroll">
205
+ <!-- NODES -->
206
+ <div class="set-section-hdr"><span class="set-arrow open">&#9654;</span> NODES <button class="set-rst-btn" data-group="nodes">RST</button></div>
207
+ <div class="set-section-body">
208
+ <div class="set-row"><label>Size Min</label><input type="range" id="set-node-sizeMin" min="5" max="100" step="1"><span class="set-val"></span></div>
209
+ <div class="set-row"><label>Size Multiplier</label><input type="range" id="set-node-sizeMultiplier" min="0.5" max="10" step="0.1"><span class="set-val"></span></div>
210
+ <div class="set-row"><label>Opacity</label><input type="range" id="set-node-opacity" min="0.1" max="1" step="0.05"><span class="set-val"></span></div>
211
+ <div class="set-row"><label>Resolution</label><input type="range" id="set-node-resolution" min="4" max="32" step="2"><span class="set-val"></span></div>
212
+ <div class="set-row"><label>Selected Color</label><input type="color" id="set-node-selectedColor"><span class="set-val"></span></div>
213
+ </div>
214
+
215
+ <!-- LINKS -->
216
+ <div class="set-section-hdr"><span class="set-arrow open">&#9654;</span> LINKS <button class="set-rst-btn" data-group="links">RST</button></div>
217
+ <div class="set-section-body">
218
+ <div class="set-row"><label>Normal Width</label><input type="range" id="set-link-normalWidth" min="0.1" max="8" step="0.1"><span class="set-val"></span></div>
219
+ <div class="set-row"><label>Highlight Width</label><input type="range" id="set-link-highlightWidth" min="1" max="12" step="0.5"><span class="set-val"></span></div>
220
+ <div class="set-row"><label>Opacity</label><input type="range" id="set-link-opacity" min="0.1" max="1" step="0.05"><span class="set-val"></span></div>
221
+ <div class="set-row"><label>Curvature</label><input type="range" id="set-link-curvatureBase" min="0" max="0.5" step="0.01"><span class="set-val"></span></div>
222
+ <div class="set-row"><label>Normal Alpha</label><input type="range" id="set-link-normalAlpha" min="0.05" max="1" step="0.05"><span class="set-val"></span></div>
223
+ <div class="set-row"><label>Highlight Color</label><input type="color" id="set-link-highlightColor"><span class="set-val"></span></div>
224
+ </div>
225
+
226
+ <!-- PARTICLES -->
227
+ <div class="set-section-hdr"><span class="set-arrow open">&#9654;</span> PARTICLES <button class="set-rst-btn" data-group="particles">RST</button></div>
228
+ <div class="set-section-body">
229
+ <div class="set-row"><label>Normal Count</label><input type="range" id="set-part-normalCount" min="0" max="20" step="1"><span class="set-val"></span></div>
230
+ <div class="set-row"><label>Highlight Count</label><input type="range" id="set-part-highlightCount" min="0" max="30" step="1"><span class="set-val"></span></div>
231
+ <div class="set-row"><label>Normal Width</label><input type="range" id="set-part-normalWidth" min="0.5" max="6" step="0.1"><span class="set-val"></span></div>
232
+ <div class="set-row"><label>Highlight Width</label><input type="range" id="set-part-highlightWidth" min="1" max="10" step="0.5"><span class="set-val"></span></div>
233
+ <div class="set-row"><label>Normal Speed</label><input type="range" id="set-part-normalSpeed" min="0.001" max="0.02" step="0.001"><span class="set-val"></span></div>
234
+ <div class="set-row"><label>Highlight Speed</label><input type="range" id="set-part-highlightSpeed" min="0.001" max="0.02" step="0.001"><span class="set-val"></span></div>
235
+ <div class="set-row"><label>Highlight Color</label><input type="color" id="set-part-highlightColor"><span class="set-val"></span></div>
236
+ </div>
237
+
238
+ <!-- LAYER COLORS -->
239
+ <div class="set-section-hdr"><span class="set-arrow open">&#9654;</span> LAYER COLORS <button class="set-rst-btn" data-group="colors">RST</button></div>
240
+ <div class="set-section-body">
241
+ <div class="set-row"><label>FE (Frontend)</label><input type="color" id="set-color-FE"><span class="set-val"></span></div>
242
+ <div class="set-row"><label>BE (Backend)</label><input type="color" id="set-color-BE"><span class="set-val"></span></div>
243
+ <div class="set-row"><label>DB (Database)</label><input type="color" id="set-color-DB"><span class="set-val"></span></div>
244
+ <div class="set-row"><label>API</label><input type="color" id="set-color-API"><span class="set-val"></span></div>
245
+ <div class="set-row"><label>CONFIG</label><input type="color" id="set-color-CONFIG"><span class="set-val"></span></div>
246
+ <div class="set-row"><label>UTIL</label><input type="color" id="set-color-UTIL"><span class="set-val"></span></div>
247
+ </div>
248
+
249
+ <!-- SCENE -->
250
+ <div class="set-section-hdr"><span class="set-arrow open">&#9654;</span> SCENE <button class="set-rst-btn" data-group="scene">RST</button></div>
251
+ <div class="set-section-body">
252
+ <div class="set-row"><label>Background</label><input type="color" id="set-scene-background"><span class="set-val"></span></div>
253
+ <div class="set-row"><label>Ambient Light</label><input type="range" id="set-scene-ambientIntensity" min="0" max="20" step="0.5"><span class="set-val"></span></div>
254
+ <div class="set-row"><label>Point Light</label><input type="range" id="set-scene-pointIntensity" min="0" max="10" step="0.5"><span class="set-val"></span></div>
255
+ <div class="set-row"><label>Light Distance</label><input type="range" id="set-scene-pointDistance" min="1000" max="20000" step="500"><span class="set-val"></span></div>
256
+ <div class="set-row"><label>Fog Density</label><input type="range" id="set-scene-fogDensity" min="0" max="0.001" step="0.00001"><span class="set-val"></span></div>
257
+ <div class="set-row"><label>Scanline</label><input type="range" id="set-scene-scanlineOpacity" min="0" max="1" step="0.05"><span class="set-val"></span></div>
258
+ </div>
259
+
260
+ <!-- CAMERA -->
261
+ <div class="set-section-hdr"><span class="set-arrow open">&#9654;</span> CAMERA <button class="set-rst-btn" data-group="camera">RST</button></div>
262
+ <div class="set-section-body">
263
+ <div class="set-row"><label>Initial Distance</label><input type="range" id="set-cam-initialZ" min="500" max="8000" step="100"><span class="set-val"></span></div>
264
+ <div class="set-row"><label>Rotate Speed</label><input type="range" id="set-cam-autoRotateSpeed" min="0.0001" max="0.003" step="0.0001"><span class="set-val"></span></div>
265
+ <div class="set-row"><label>Orbit Radius</label><input type="range" id="set-cam-autoRotateRadius" min="400" max="5000" step="100"><span class="set-val"></span></div>
266
+ <div class="set-row"><label>Reset Distance</label><input type="range" id="set-cam-resetDistance" min="500" max="5000" step="100"><span class="set-val"></span></div>
267
+ </div>
268
+
269
+ <!-- ADVANCED (collapsed by default) -->
270
+ <div class="set-section-hdr"><span class="set-arrow">&#9654;</span> ADVANCED <button class="set-rst-btn" data-group="physics">RST</button></div>
271
+ <div class="set-section-body collapsed">
272
+ <div class="set-sub-title">PHYSICS</div>
273
+ <div class="set-row"><label>Alpha Decay</label><input type="range" id="set-phys-alphaDecay" min="0.001" max="0.05" step="0.001"><span class="set-val"></span></div>
274
+ <div class="set-row"><label>Velocity Decay</label><input type="range" id="set-phys-velocityDecay" min="0.05" max="0.8" step="0.01"><span class="set-val"></span></div>
275
+ <div class="set-row"><label>Charge Strength</label><input type="range" id="set-phys-chargeStrength" min="-3000" max="-100" step="50"><span class="set-val"></span></div>
276
+ <div class="set-row"><label>Same-Layer Dist</label><input type="range" id="set-phys-sameLayerDistance" min="50" max="800" step="10"><span class="set-val"></span></div>
277
+ <div class="set-row"><label>Cross-Layer Dist</label><input type="range" id="set-phys-crossLayerDistance" min="200" max="3000" step="50"><span class="set-val"></span></div>
278
+ <div class="set-row"><label>Cluster Force</label><input type="range" id="set-phys-clusterStrength" min="0" max="0.1" step="0.001"><span class="set-val"></span></div>
279
+ <div class="set-sub-title">ANIMATION</div>
280
+ <div class="set-row"><label>Birth Duration</label><input type="range" id="set-anim-birthDuration" min="500" max="5000" step="100"><span class="set-val"></span></div>
281
+ <div class="set-row"><label>Birth Scale</label><input type="range" id="set-anim-birthScale" min="1" max="8" step="0.5"><span class="set-val"></span></div>
282
+ <div class="set-row"><label>Spawn X</label><input type="range" id="set-anim-spawnX" min="-10000" max="10000" step="500"><span class="set-val"></span></div>
283
+ <div class="set-row"><label>Spawn Y</label><input type="range" id="set-anim-spawnY" min="-10000" max="10000" step="500"><span class="set-val"></span></div>
284
+ <div class="set-row"><label>Spawn Z</label><input type="range" id="set-anim-spawnZ" min="-10000" max="10000" step="500"><span class="set-val"></span></div>
285
+ <div class="set-sub-title">ARROWS</div>
286
+ <div class="set-row"><label>Arrow Length</label><input type="range" id="set-arrow-length" min="0" max="20" step="1"><span class="set-val"></span></div>
287
+ <div class="set-row"><label>Arrow Position</label><input type="range" id="set-arrow-position" min="0" max="1" step="0.05"><span class="set-val"></span></div>
288
+ </div>
289
+ </div>
290
+ </div>
291
+ </div>
195
292
  </aside>
196
293
  </main>
197
294
 
@@ -123,17 +123,15 @@ body {
123
123
  text-transform: uppercase;
124
124
  }
125
125
 
126
- /* ── License Badge ── */
126
+ /* ── License Badge (inline, next to SSE indicator) ── */
127
127
  .license-badge {
128
- display: flex;
129
- align-items: center;
130
- gap: 8px;
131
- padding: 5px 16px;
132
- border-radius: 20px;
133
- font-size: 11px;
128
+ font-size: 9px;
129
+ letter-spacing: 1.5px;
130
+ padding: 2px 8px;
131
+ border-radius: 3px;
132
+ border: 1px solid var(--border);
133
+ margin-left: 8px;
134
134
  font-weight: 700;
135
- letter-spacing: 2px;
136
- border: 1px solid;
137
135
  transition: all 0.3s;
138
136
  cursor: default;
139
137
  }
@@ -141,50 +139,22 @@ body {
141
139
  .license-badge.free {
142
140
  color: #8899aa;
143
141
  border-color: rgba(136,153,170,0.4);
144
- background: rgba(136,153,170,0.12);
145
142
  }
146
143
 
147
144
  .license-badge.pro {
148
145
  color: #ffd700;
149
146
  border-color: rgba(255,215,0,0.4);
150
- background: rgba(255,215,0,0.08);
151
147
  text-shadow: 0 0 8px rgba(255,215,0,0.3);
152
148
  }
153
149
 
154
150
  .license-badge.trial {
155
151
  color: var(--risk-medium);
156
152
  border-color: rgba(255,159,10,0.4);
157
- background: rgba(255,159,10,0.08);
158
- }
159
-
160
- .license-badge.trial-urgent {
161
- color: var(--risk-high);
162
- border-color: rgba(255,45,85,0.4);
163
- background: rgba(255,45,85,0.08);
164
- animation: license-urgent 1.5s ease-in-out infinite;
165
153
  }
166
154
 
167
155
  .license-badge.expired {
168
156
  color: var(--risk-high);
169
157
  border-color: rgba(255,45,85,0.4);
170
- background: rgba(255,45,85,0.1);
171
- }
172
-
173
- @keyframes license-urgent {
174
- 0%, 100% { opacity: 1; }
175
- 50% { opacity: 0.6; }
176
- }
177
-
178
- .license-plan {
179
- font-weight: 700;
180
- }
181
-
182
- .license-timer {
183
- font-size: 9px;
184
- font-weight: 400;
185
- letter-spacing: 1px;
186
- opacity: 0.85;
187
- font-variant-numeric: tabular-nums;
188
158
  }
189
159
 
190
160
  .top-controls {
@@ -2342,3 +2312,244 @@ main {
2342
2312
  z-index: 50;
2343
2313
  backdrop-filter: blur(8px);
2344
2314
  }
2315
+
2316
+ /* ═══════════════════════════════════════════ */
2317
+ /* SETTINGS PANEL */
2318
+ /* ═══════════════════════════════════════════ */
2319
+ #tab-settings {
2320
+ overflow: hidden;
2321
+ }
2322
+
2323
+ .set-panel {
2324
+ display: flex;
2325
+ flex-direction: column;
2326
+ height: 100%;
2327
+ overflow: hidden;
2328
+ }
2329
+
2330
+ .set-actions {
2331
+ display: flex;
2332
+ gap: 6px;
2333
+ padding: 10px 14px;
2334
+ border-bottom: 1px solid var(--border);
2335
+ flex-shrink: 0;
2336
+ }
2337
+
2338
+ .set-action-btn {
2339
+ flex: 1;
2340
+ padding: 6px 0;
2341
+ background: transparent;
2342
+ color: var(--text-secondary);
2343
+ border: 1px solid var(--border);
2344
+ border-radius: 2px;
2345
+ cursor: pointer;
2346
+ font-size: 9px;
2347
+ font-family: inherit;
2348
+ font-weight: 600;
2349
+ letter-spacing: 2px;
2350
+ transition: all 0.2s;
2351
+ }
2352
+
2353
+ .set-action-btn:hover {
2354
+ color: var(--accent);
2355
+ border-color: var(--accent);
2356
+ box-shadow: var(--glow-cyan);
2357
+ }
2358
+
2359
+ .set-action-btn.set-danger:hover {
2360
+ color: var(--risk-high);
2361
+ border-color: var(--risk-high);
2362
+ box-shadow: var(--glow-red);
2363
+ }
2364
+
2365
+ .set-scroll {
2366
+ flex: 1;
2367
+ overflow-y: auto;
2368
+ padding: 4px 0;
2369
+ }
2370
+
2371
+ .set-scroll::-webkit-scrollbar { width: 4px; }
2372
+ .set-scroll::-webkit-scrollbar-track { background: transparent; }
2373
+ .set-scroll::-webkit-scrollbar-thumb { background: var(--accent-dim); border-radius: 2px; }
2374
+
2375
+ /* Section header */
2376
+ .set-section-hdr {
2377
+ display: flex;
2378
+ align-items: center;
2379
+ gap: 8px;
2380
+ padding: 8px 14px;
2381
+ cursor: pointer;
2382
+ font-size: 10px;
2383
+ font-weight: 700;
2384
+ letter-spacing: 2px;
2385
+ color: var(--accent);
2386
+ border-bottom: 1px solid rgba(26,45,77,0.3);
2387
+ user-select: none;
2388
+ transition: background 0.2s;
2389
+ }
2390
+
2391
+ .set-section-hdr:hover {
2392
+ background: rgba(0,212,255,0.04);
2393
+ }
2394
+
2395
+ .set-arrow {
2396
+ display: inline-block;
2397
+ font-size: 8px;
2398
+ transition: transform 0.25s ease;
2399
+ color: var(--text-secondary);
2400
+ }
2401
+
2402
+ .set-arrow.open {
2403
+ transform: rotate(90deg);
2404
+ }
2405
+
2406
+ .set-rst-btn {
2407
+ margin-left: auto;
2408
+ padding: 2px 8px;
2409
+ background: transparent;
2410
+ color: var(--text-secondary);
2411
+ border: 1px solid var(--border);
2412
+ border-radius: 2px;
2413
+ cursor: pointer;
2414
+ font-size: 8px;
2415
+ font-family: inherit;
2416
+ font-weight: 600;
2417
+ letter-spacing: 1px;
2418
+ transition: all 0.2s;
2419
+ }
2420
+
2421
+ .set-rst-btn:hover {
2422
+ color: var(--risk-medium);
2423
+ border-color: var(--risk-medium);
2424
+ }
2425
+
2426
+ /* Section body */
2427
+ .set-section-body {
2428
+ padding: 6px 14px;
2429
+ border-bottom: 1px solid rgba(26,45,77,0.2);
2430
+ }
2431
+
2432
+ .set-section-body.collapsed {
2433
+ display: none;
2434
+ }
2435
+
2436
+ .set-sub-title {
2437
+ font-size: 9px;
2438
+ font-weight: 600;
2439
+ letter-spacing: 2px;
2440
+ color: var(--text-secondary);
2441
+ margin: 8px 0 4px;
2442
+ padding-top: 6px;
2443
+ border-top: 1px solid rgba(26,45,77,0.3);
2444
+ }
2445
+
2446
+ .set-sub-title:first-child {
2447
+ margin-top: 0;
2448
+ padding-top: 0;
2449
+ border-top: none;
2450
+ }
2451
+
2452
+ /* Individual control row */
2453
+ .set-row {
2454
+ display: flex;
2455
+ align-items: center;
2456
+ gap: 8px;
2457
+ padding: 3px 0;
2458
+ }
2459
+
2460
+ .set-row label {
2461
+ font-size: 10px;
2462
+ color: var(--text-secondary);
2463
+ letter-spacing: 0.5px;
2464
+ min-width: 100px;
2465
+ flex-shrink: 0;
2466
+ }
2467
+
2468
+ .set-row .set-val {
2469
+ font-size: 9px;
2470
+ color: var(--accent);
2471
+ min-width: 48px;
2472
+ text-align: right;
2473
+ font-variant-numeric: tabular-nums;
2474
+ flex-shrink: 0;
2475
+ }
2476
+
2477
+ /* Range slider styling */
2478
+ .set-row input[type="range"] {
2479
+ -webkit-appearance: none;
2480
+ appearance: none;
2481
+ flex: 1;
2482
+ height: 3px;
2483
+ background: var(--border);
2484
+ border-radius: 2px;
2485
+ outline: none;
2486
+ cursor: pointer;
2487
+ }
2488
+
2489
+ .set-row input[type="range"]::-webkit-slider-thumb {
2490
+ -webkit-appearance: none;
2491
+ appearance: none;
2492
+ width: 12px;
2493
+ height: 12px;
2494
+ border-radius: 50%;
2495
+ background: var(--accent);
2496
+ border: 2px solid var(--bg-primary);
2497
+ box-shadow: 0 0 6px rgba(0,212,255,0.5);
2498
+ cursor: pointer;
2499
+ transition: box-shadow 0.2s, transform 0.15s;
2500
+ }
2501
+
2502
+ .set-row input[type="range"]::-webkit-slider-thumb:hover {
2503
+ box-shadow: 0 0 12px rgba(0,212,255,0.8);
2504
+ transform: scale(1.2);
2505
+ }
2506
+
2507
+ .set-row input[type="range"]:active::-webkit-slider-thumb {
2508
+ box-shadow: 0 0 16px rgba(0,212,255,1);
2509
+ transform: scale(1.3);
2510
+ }
2511
+
2512
+ .set-row input[type="range"]::-moz-range-thumb {
2513
+ width: 12px;
2514
+ height: 12px;
2515
+ border-radius: 50%;
2516
+ background: var(--accent);
2517
+ border: 2px solid var(--bg-primary);
2518
+ box-shadow: 0 0 6px rgba(0,212,255,0.5);
2519
+ cursor: pointer;
2520
+ }
2521
+
2522
+ .set-row input[type="range"]::-moz-range-track {
2523
+ height: 3px;
2524
+ background: var(--border);
2525
+ border-radius: 2px;
2526
+ border: none;
2527
+ }
2528
+
2529
+ /* Color picker styling */
2530
+ .set-row input[type="color"] {
2531
+ -webkit-appearance: none;
2532
+ appearance: none;
2533
+ width: 28px;
2534
+ height: 20px;
2535
+ border: 1px solid var(--border);
2536
+ border-radius: 2px;
2537
+ background: transparent;
2538
+ cursor: pointer;
2539
+ padding: 1px;
2540
+ transition: border-color 0.2s, box-shadow 0.2s;
2541
+ }
2542
+
2543
+ .set-row input[type="color"]:hover {
2544
+ border-color: var(--accent);
2545
+ box-shadow: 0 0 8px rgba(0,212,255,0.4);
2546
+ }
2547
+
2548
+ .set-row input[type="color"]::-webkit-color-swatch-wrapper {
2549
+ padding: 0;
2550
+ }
2551
+
2552
+ .set-row input[type="color"]::-webkit-color-swatch {
2553
+ border: none;
2554
+ border-radius: 1px;
2555
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syke1/mcp-server",
3
- "version": "1.3.19",
3
+ "version": "1.4.0",
4
4
  "mcpName": "io.github.khalomsky/syke",
5
5
  "description": "AI code impact analysis MCP server — dependency graphs, cascade detection, and a mandatory build gate for AI coding agents",
6
6
  "main": "dist/index.js",