sverklo 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/sverklo.js +194 -9
- package/dist/bin/sverklo.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/registry/indexer-pool.d.ts +24 -0
- package/dist/src/registry/indexer-pool.js +97 -0
- package/dist/src/registry/indexer-pool.js.map +1 -0
- package/dist/src/registry/registry.d.ts +18 -0
- package/dist/src/registry/registry.js +69 -0
- package/dist/src/registry/registry.js.map +1 -0
- package/dist/src/registry/registry.test.d.ts +1 -0
- package/dist/src/registry/registry.test.js +79 -0
- package/dist/src/registry/registry.test.js.map +1 -0
- package/dist/src/search/cluster.d.ts +25 -0
- package/dist/src/search/cluster.js +216 -0
- package/dist/src/search/cluster.js.map +1 -0
- package/dist/src/server/dashboard-html.js +324 -183
- package/dist/src/server/dashboard-html.js.map +1 -1
- package/dist/src/server/http-server.js +29 -0
- package/dist/src/server/http-server.js.map +1 -1
- package/dist/src/server/mcp-server.d.ts +1 -0
- package/dist/src/server/mcp-server.js +319 -0
- package/dist/src/server/mcp-server.js.map +1 -1
- package/dist/src/server/tools/clusters.d.ts +20 -0
- package/dist/src/server/tools/clusters.js +92 -0
- package/dist/src/server/tools/clusters.js.map +1 -0
- package/dist/src/server/tools/list-repos.d.ts +9 -0
- package/dist/src/server/tools/list-repos.js +48 -0
- package/dist/src/server/tools/list-repos.js.map +1 -0
- package/dist/src/wiki/wiki-generator.d.ts +12 -0
- package/dist/src/wiki/wiki-generator.js +357 -0
- package/dist/src/wiki/wiki-generator.js.map +1 -0
- package/package.json +1 -1
|
@@ -181,14 +181,31 @@ main.stage {
|
|
|
181
181
|
.view.active { display: block; }
|
|
182
182
|
|
|
183
183
|
/* ── Graph view ── */
|
|
184
|
-
#graph-view { position: relative; }
|
|
185
|
-
#graph-
|
|
184
|
+
#graph-view { position: relative; overflow: hidden; }
|
|
185
|
+
#graph-svg {
|
|
186
186
|
width: 100%;
|
|
187
187
|
height: 100%;
|
|
188
188
|
display: block;
|
|
189
189
|
cursor: grab;
|
|
190
190
|
}
|
|
191
|
-
#graph-
|
|
191
|
+
#graph-svg:active { cursor: grabbing; }
|
|
192
|
+
#graph-svg .node { cursor: pointer; }
|
|
193
|
+
#graph-svg .node circle { stroke: var(--bg); stroke-width: 1.5px; transition: stroke 0.15s; }
|
|
194
|
+
#graph-svg .node:hover circle { stroke: var(--accent); stroke-width: 2px; }
|
|
195
|
+
#graph-svg .node.selected circle { stroke: var(--accent); stroke-width: 2.5px; }
|
|
196
|
+
#graph-svg .link { stroke: var(--rule); fill: none; }
|
|
197
|
+
#graph-svg .link.highlighted { stroke: var(--accent); }
|
|
198
|
+
#graph-svg .node-label {
|
|
199
|
+
font-family: 'JetBrains Mono', monospace;
|
|
200
|
+
font-size: 10px;
|
|
201
|
+
fill: var(--text-2);
|
|
202
|
+
pointer-events: none;
|
|
203
|
+
opacity: 0;
|
|
204
|
+
transition: opacity 0.15s;
|
|
205
|
+
}
|
|
206
|
+
#graph-svg .node:hover .node-label,
|
|
207
|
+
#graph-svg .node.selected .node-label,
|
|
208
|
+
#graph-svg .node.label-visible .node-label { opacity: 1; }
|
|
192
209
|
|
|
193
210
|
.graph-controls {
|
|
194
211
|
position: absolute;
|
|
@@ -198,6 +215,9 @@ main.stage {
|
|
|
198
215
|
gap: 8px;
|
|
199
216
|
font-family: 'JetBrains Mono', monospace;
|
|
200
217
|
font-size: 11px;
|
|
218
|
+
align-items: center;
|
|
219
|
+
flex-wrap: wrap;
|
|
220
|
+
max-width: 500px;
|
|
201
221
|
}
|
|
202
222
|
.graph-chip {
|
|
203
223
|
padding: 6px 12px;
|
|
@@ -233,6 +253,69 @@ main.stage {
|
|
|
233
253
|
.graph-search input:focus { border-color: var(--accent); }
|
|
234
254
|
.graph-search input::placeholder { color: var(--text-3); }
|
|
235
255
|
|
|
256
|
+
.graph-legend {
|
|
257
|
+
position: absolute;
|
|
258
|
+
top: 20px;
|
|
259
|
+
right: 20px;
|
|
260
|
+
background: rgba(22, 20, 15, 0.92);
|
|
261
|
+
backdrop-filter: blur(8px);
|
|
262
|
+
border: 1px solid var(--rule);
|
|
263
|
+
padding: 12px 16px;
|
|
264
|
+
font-family: 'JetBrains Mono', monospace;
|
|
265
|
+
font-size: 10px;
|
|
266
|
+
color: var(--text-3);
|
|
267
|
+
display: flex;
|
|
268
|
+
flex-direction: column;
|
|
269
|
+
gap: 6px;
|
|
270
|
+
}
|
|
271
|
+
.graph-legend-item {
|
|
272
|
+
display: flex;
|
|
273
|
+
align-items: center;
|
|
274
|
+
gap: 8px;
|
|
275
|
+
}
|
|
276
|
+
.graph-legend-dot {
|
|
277
|
+
width: 8px;
|
|
278
|
+
height: 8px;
|
|
279
|
+
border-radius: 50%;
|
|
280
|
+
flex-shrink: 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.graph-slider-group {
|
|
284
|
+
display: flex;
|
|
285
|
+
align-items: center;
|
|
286
|
+
gap: 8px;
|
|
287
|
+
padding: 6px 12px;
|
|
288
|
+
background: var(--bg-2);
|
|
289
|
+
border: 1px solid var(--rule);
|
|
290
|
+
color: var(--text-2);
|
|
291
|
+
}
|
|
292
|
+
.graph-slider-group label { white-space: nowrap; }
|
|
293
|
+
.graph-slider-group input[type="range"] {
|
|
294
|
+
width: 100px;
|
|
295
|
+
accent-color: var(--accent);
|
|
296
|
+
}
|
|
297
|
+
.graph-slider-group .slider-val {
|
|
298
|
+
min-width: 32px;
|
|
299
|
+
text-align: right;
|
|
300
|
+
color: var(--accent);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.graph-tooltip {
|
|
304
|
+
position: absolute;
|
|
305
|
+
pointer-events: none;
|
|
306
|
+
background: rgba(22, 20, 15, 0.95);
|
|
307
|
+
border: 1px solid var(--rule-2);
|
|
308
|
+
padding: 8px 12px;
|
|
309
|
+
font-family: 'JetBrains Mono', monospace;
|
|
310
|
+
font-size: 11px;
|
|
311
|
+
color: var(--text);
|
|
312
|
+
display: none;
|
|
313
|
+
z-index: 100;
|
|
314
|
+
white-space: nowrap;
|
|
315
|
+
}
|
|
316
|
+
.graph-tooltip .tt-path { color: var(--accent); margin-bottom: 2px; }
|
|
317
|
+
.graph-tooltip .tt-meta { color: var(--text-3); }
|
|
318
|
+
|
|
236
319
|
/* ── Search view ── */
|
|
237
320
|
#search-view {
|
|
238
321
|
display: none;
|
|
@@ -642,15 +725,23 @@ footer.status .spacer { flex: 1; }
|
|
|
642
725
|
<main class="stage">
|
|
643
726
|
<!-- Graph View -->
|
|
644
727
|
<div class="view active" id="graph-view">
|
|
645
|
-
<
|
|
728
|
+
<svg id="graph-svg"></svg>
|
|
646
729
|
<div class="graph-search">
|
|
647
730
|
<input type="text" id="graph-filter" placeholder="filter nodes…" />
|
|
648
731
|
</div>
|
|
649
732
|
<div class="graph-controls">
|
|
650
|
-
<div class="graph-chip on"
|
|
651
|
-
<div class="graph-chip"
|
|
652
|
-
<div class="graph-
|
|
653
|
-
|
|
733
|
+
<div class="graph-chip on" id="graph-top100" onclick="graphLoadTop()">top 100</div>
|
|
734
|
+
<div class="graph-chip" id="graph-showall" onclick="graphLoadAll()">show all</div>
|
|
735
|
+
<div class="graph-slider-group">
|
|
736
|
+
<label>min PR</label>
|
|
737
|
+
<input type="range" id="graph-pr-slider" min="0" max="100" value="0" />
|
|
738
|
+
<span class="slider-val" id="graph-pr-val">0.00</span>
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
<div class="graph-legend" id="graph-legend"></div>
|
|
742
|
+
<div class="graph-tooltip" id="graph-tooltip">
|
|
743
|
+
<div class="tt-path"></div>
|
|
744
|
+
<div class="tt-meta"></div>
|
|
654
745
|
</div>
|
|
655
746
|
</div>
|
|
656
747
|
|
|
@@ -727,6 +818,7 @@ footer.status .spacer { flex: 1; }
|
|
|
727
818
|
</div>
|
|
728
819
|
</div>
|
|
729
820
|
|
|
821
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
|
730
822
|
<script>
|
|
731
823
|
// ────────── STATE ──────────
|
|
732
824
|
let state = {
|
|
@@ -773,9 +865,8 @@ async function init() {
|
|
|
773
865
|
renderInspectorToday();
|
|
774
866
|
renderStats();
|
|
775
867
|
|
|
776
|
-
// Load graph
|
|
777
|
-
|
|
778
|
-
drawGraph();
|
|
868
|
+
// Load graph (D3 force-directed)
|
|
869
|
+
await graphLoadTop();
|
|
779
870
|
|
|
780
871
|
// Rail navigation
|
|
781
872
|
document.querySelectorAll('.rail-item').forEach(el => {
|
|
@@ -788,7 +879,12 @@ async function init() {
|
|
|
788
879
|
// Graph filter
|
|
789
880
|
document.getElementById('graph-filter').addEventListener('input', (e) => {
|
|
790
881
|
state.graphFilter = e.target.value.toLowerCase();
|
|
791
|
-
|
|
882
|
+
graphApplyFilter();
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// PageRank slider
|
|
886
|
+
document.getElementById('graph-pr-slider').addEventListener('input', (e) => {
|
|
887
|
+
graphApplyFilter();
|
|
792
888
|
});
|
|
793
889
|
|
|
794
890
|
// Cmdk
|
|
@@ -802,8 +898,7 @@ async function init() {
|
|
|
802
898
|
});
|
|
803
899
|
document.getElementById('cmdk-input').addEventListener('input', (e) => runCmdk(e.target.value));
|
|
804
900
|
|
|
805
|
-
// Resize
|
|
806
|
-
window.addEventListener('resize', () => { if (state.currentView === 'graph') drawGraph(); });
|
|
901
|
+
// Resize handled by D3 (SVG scales with container)
|
|
807
902
|
}
|
|
808
903
|
|
|
809
904
|
function switchView(view) {
|
|
@@ -811,188 +906,234 @@ function switchView(view) {
|
|
|
811
906
|
document.querySelectorAll('.rail-item').forEach(el => el.classList.toggle('active', el.dataset.view === view));
|
|
812
907
|
document.querySelectorAll('.view').forEach(el => el.classList.toggle('active', el.id === view + '-view'));
|
|
813
908
|
|
|
814
|
-
if (view === 'graph')
|
|
909
|
+
if (view === 'graph' && !state.graphData) graphLoadTop();
|
|
815
910
|
if (view === 'files') renderFiles();
|
|
816
911
|
if (view === 'memories') renderMemories();
|
|
817
912
|
if (view === 'search') document.getElementById('search-input').focus();
|
|
818
913
|
if (view === 'stats') renderStats();
|
|
819
914
|
}
|
|
820
915
|
|
|
821
|
-
// ────────── GRAPH (
|
|
822
|
-
let
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
916
|
+
// ────────── GRAPH (D3 force-directed) ──────────
|
|
917
|
+
let graphSim = null;
|
|
918
|
+
let graphSvgGroup = null;
|
|
919
|
+
let graphZoom = null;
|
|
920
|
+
let graphNodeSel = null;
|
|
921
|
+
let graphLinkSel = null;
|
|
922
|
+
let graphSelectedNode = null;
|
|
923
|
+
let graphIsShowAll = false;
|
|
924
|
+
|
|
925
|
+
async function graphLoadTop() {
|
|
926
|
+
document.getElementById('graph-top100').classList.add('on');
|
|
927
|
+
document.getElementById('graph-showall').classList.remove('on');
|
|
928
|
+
graphIsShowAll = false;
|
|
929
|
+
state.graphData = await api('/api/graph?limit=100');
|
|
930
|
+
initD3Graph();
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async function graphLoadAll() {
|
|
934
|
+
document.getElementById('graph-showall').classList.add('on');
|
|
935
|
+
document.getElementById('graph-top100').classList.remove('on');
|
|
936
|
+
graphIsShowAll = true;
|
|
937
|
+
state.graphData = await api('/api/graph?limit=0');
|
|
938
|
+
initD3Graph();
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function nodeRadius(d) {
|
|
942
|
+
// Scale pagerank to 4-24px radius
|
|
943
|
+
const pr = d.pagerank || 0;
|
|
944
|
+
const maxPR = state.graphData ? Math.max(...state.graphData.nodes.map(n => n.pagerank || 0), 0.001) : 1;
|
|
945
|
+
return 4 + (pr / maxPR) * 20;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function edgeOpacity(d) {
|
|
949
|
+
if (!state.graphData) return 0.15;
|
|
950
|
+
const maxW = Math.max(...state.graphData.edges.map(e => e.weight || 1), 1);
|
|
951
|
+
return 0.08 + (d.weight / maxW) * 0.5;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function initD3Graph() {
|
|
955
|
+
const container = document.getElementById('graph-view');
|
|
956
|
+
const svg = d3.select('#graph-svg');
|
|
957
|
+
svg.selectAll('*').remove();
|
|
958
|
+
if (graphSim) { graphSim.stop(); graphSim = null; }
|
|
959
|
+
|
|
960
|
+
const w = container.clientWidth;
|
|
961
|
+
const h = container.clientHeight;
|
|
962
|
+
svg.attr('width', w).attr('height', h).attr('viewBox', [0, 0, w, h]);
|
|
963
|
+
|
|
964
|
+
if (!state.graphData || !state.graphData.nodes.length) return;
|
|
965
|
+
|
|
966
|
+
// Build node map for edge resolution (edges use numeric IDs)
|
|
967
|
+
const nodeById = new Map(state.graphData.nodes.map(n => [n.id, n]));
|
|
968
|
+
const nodes = state.graphData.nodes.map(n => ({ ...n }));
|
|
969
|
+
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
|
970
|
+
const links = state.graphData.edges
|
|
971
|
+
.filter(e => nodeMap.has(e.source) && nodeMap.has(e.target))
|
|
972
|
+
.map(e => ({ source: e.source, target: e.target, weight: e.weight }));
|
|
973
|
+
|
|
974
|
+
// Set up zoom
|
|
975
|
+
graphZoom = d3.zoom()
|
|
976
|
+
.scaleExtent([0.1, 8])
|
|
977
|
+
.on('zoom', (event) => {
|
|
978
|
+
graphSvgGroup.attr('transform', event.transform);
|
|
979
|
+
});
|
|
980
|
+
svg.call(graphZoom);
|
|
981
|
+
|
|
982
|
+
graphSvgGroup = svg.append('g');
|
|
983
|
+
|
|
984
|
+
// Links
|
|
985
|
+
graphLinkSel = graphSvgGroup.append('g').attr('class', 'links')
|
|
986
|
+
.selectAll('line')
|
|
987
|
+
.data(links)
|
|
988
|
+
.join('line')
|
|
989
|
+
.attr('class', 'link')
|
|
990
|
+
.attr('stroke-opacity', d => edgeOpacity(d))
|
|
991
|
+
.attr('stroke-width', d => Math.max(0.5, Math.min(3, d.weight * 0.5)));
|
|
992
|
+
|
|
993
|
+
// Nodes
|
|
994
|
+
graphNodeSel = graphSvgGroup.append('g').attr('class', 'nodes')
|
|
995
|
+
.selectAll('g')
|
|
996
|
+
.data(nodes, d => d.id)
|
|
997
|
+
.join('g')
|
|
998
|
+
.attr('class', d => {
|
|
999
|
+
let cls = 'node';
|
|
1000
|
+
if ((d.pagerank || 0) > 0.3) cls += ' label-visible';
|
|
1001
|
+
return cls;
|
|
1002
|
+
})
|
|
1003
|
+
.call(d3.drag()
|
|
1004
|
+
.on('start', dragStart)
|
|
1005
|
+
.on('drag', dragging)
|
|
1006
|
+
.on('end', dragEnd));
|
|
1007
|
+
|
|
1008
|
+
graphNodeSel.append('circle')
|
|
1009
|
+
.attr('r', d => nodeRadius(d))
|
|
1010
|
+
.attr('fill', d => getLangColor(d.language));
|
|
1011
|
+
|
|
1012
|
+
graphNodeSel.append('text')
|
|
1013
|
+
.attr('class', 'node-label')
|
|
1014
|
+
.attr('dx', d => nodeRadius(d) + 4)
|
|
1015
|
+
.attr('dy', 3)
|
|
1016
|
+
.text(d => d.path.split('/').pop());
|
|
1017
|
+
|
|
1018
|
+
// Hover
|
|
1019
|
+
const tooltip = document.getElementById('graph-tooltip');
|
|
1020
|
+
graphNodeSel
|
|
1021
|
+
.on('mouseover', function(event, d) {
|
|
1022
|
+
d3.select(this).raise();
|
|
1023
|
+
// Highlight connected edges
|
|
1024
|
+
graphLinkSel
|
|
1025
|
+
.classed('highlighted', l => l.source.id === d.id || l.target.id === d.id)
|
|
1026
|
+
.attr('stroke-opacity', l => (l.source.id === d.id || l.target.id === d.id) ? 0.9 : edgeOpacity(l));
|
|
1027
|
+
// Tooltip
|
|
1028
|
+
tooltip.style.display = 'block';
|
|
1029
|
+
tooltip.querySelector('.tt-path').textContent = d.path;
|
|
1030
|
+
tooltip.querySelector('.tt-meta').textContent =
|
|
1031
|
+
(d.language || 'unknown') + ' · PR ' + (d.pagerank || 0).toFixed(3) + ' · ' + formatBytes(d.size_bytes);
|
|
1032
|
+
})
|
|
1033
|
+
.on('mousemove', function(event) {
|
|
1034
|
+
tooltip.style.left = (event.offsetX + 16) + 'px';
|
|
1035
|
+
tooltip.style.top = (event.offsetY - 10) + 'px';
|
|
1036
|
+
})
|
|
1037
|
+
.on('mouseout', function() {
|
|
1038
|
+
graphLinkSel.classed('highlighted', false)
|
|
1039
|
+
.attr('stroke-opacity', d => edgeOpacity(d));
|
|
1040
|
+
tooltip.style.display = 'none';
|
|
1041
|
+
})
|
|
1042
|
+
.on('click', function(event, d) {
|
|
1043
|
+
event.stopPropagation();
|
|
1044
|
+
graphNodeSel.classed('selected', false);
|
|
1045
|
+
d3.select(this).classed('selected', true);
|
|
1046
|
+
graphSelectedNode = d;
|
|
1047
|
+
inspectFile(d.path);
|
|
844
1048
|
});
|
|
845
|
-
graphState.edges = state.graphData.edges;
|
|
846
|
-
const nm = {};
|
|
847
|
-
graphState.nodes.forEach(n => nm[n.path] = n);
|
|
848
|
-
|
|
849
|
-
// Run force simulation
|
|
850
|
-
for (let iter = 0; iter < 120; iter++) {
|
|
851
|
-
// Repulsion
|
|
852
|
-
for (let i = 0; i < graphState.nodes.length; i++) {
|
|
853
|
-
for (let j = i+1; j < graphState.nodes.length; j++) {
|
|
854
|
-
const a = graphState.nodes[i], b = graphState.nodes[j];
|
|
855
|
-
const dx = b.x - a.x, dy = b.y - a.y;
|
|
856
|
-
const d = Math.max(Math.sqrt(dx*dx + dy*dy), 1);
|
|
857
|
-
const f = 8000 / (d * d);
|
|
858
|
-
const fx = (dx/d) * f, fy = (dy/d) * f;
|
|
859
|
-
a.vx -= fx; a.vy -= fy;
|
|
860
|
-
b.vx += fx; b.vy += fy;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
// Attraction
|
|
864
|
-
for (const e of graphState.edges) {
|
|
865
|
-
const a = nm[e.source], b = nm[e.target];
|
|
866
|
-
if (!a || !b) continue;
|
|
867
|
-
const dx = b.x - a.x, dy = b.y - a.y;
|
|
868
|
-
const d = Math.max(Math.sqrt(dx*dx + dy*dy), 1);
|
|
869
|
-
const f = (d - 140) * 0.05;
|
|
870
|
-
const fx = (dx/d) * f, fy = (dy/d) * f;
|
|
871
|
-
a.vx += fx; a.vy += fy;
|
|
872
|
-
b.vx -= fx; b.vy -= fy;
|
|
873
|
-
}
|
|
874
|
-
// Center gravity + damping
|
|
875
|
-
for (const n of graphState.nodes) {
|
|
876
|
-
n.vx += (w/2 - n.x) * 0.008;
|
|
877
|
-
n.vy += (h/2 - n.y) * 0.008;
|
|
878
|
-
n.x += n.vx * 0.4;
|
|
879
|
-
n.y += n.vy * 0.4;
|
|
880
|
-
n.vx *= 0.85;
|
|
881
|
-
n.vy *= 0.85;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
1049
|
|
|
886
|
-
//
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const nm = {};
|
|
892
|
-
graphState.nodes.forEach(n => nm[n.path] = n);
|
|
893
|
-
ctx.lineWidth = 1;
|
|
894
|
-
for (const e of graphState.edges) {
|
|
895
|
-
const a = nm[e.source], b = nm[e.target];
|
|
896
|
-
if (!a || !b) continue;
|
|
897
|
-
const highlight = graphState.hover && (e.source === graphState.hover.path || e.target === graphState.hover.path);
|
|
898
|
-
ctx.strokeStyle = highlight ? '#E85A2A' : '#2A2620';
|
|
899
|
-
ctx.beginPath();
|
|
900
|
-
ctx.moveTo(a.x, a.y);
|
|
901
|
-
ctx.lineTo(b.x, b.y);
|
|
902
|
-
ctx.stroke();
|
|
903
|
-
}
|
|
1050
|
+
// Click background to deselect
|
|
1051
|
+
svg.on('click', () => {
|
|
1052
|
+
graphNodeSel.classed('selected', false);
|
|
1053
|
+
graphSelectedNode = null;
|
|
1054
|
+
});
|
|
904
1055
|
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
if ((n.pagerank || 0) > 0.3 || graphState.hover === n) {
|
|
921
|
-
ctx.font = '11px "JetBrains Mono", monospace';
|
|
922
|
-
ctx.fillStyle = '#A39886';
|
|
923
|
-
const label = n.path.split('/').pop();
|
|
924
|
-
ctx.fillText(label, n.x + size + 4, n.y + 3);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
ctx.globalAlpha = 1;
|
|
928
|
-
|
|
929
|
-
// Mouse handling
|
|
930
|
-
canvas.onmousemove = (ev) => {
|
|
931
|
-
const rect = canvas.getBoundingClientRect();
|
|
932
|
-
const mx = ev.clientX - rect.left;
|
|
933
|
-
const my = ev.clientY - rect.top;
|
|
934
|
-
let hit = null;
|
|
935
|
-
for (const n of graphState.nodes) {
|
|
936
|
-
const size = 3 + (n.pagerank || 0) * 12 + 3;
|
|
937
|
-
const dx = mx - n.x, dy = my - n.y;
|
|
938
|
-
if (dx*dx + dy*dy < size*size) { hit = n; break; }
|
|
939
|
-
}
|
|
940
|
-
if (hit !== graphState.hover) {
|
|
941
|
-
graphState.hover = hit;
|
|
942
|
-
canvas.style.cursor = hit ? 'pointer' : 'grab';
|
|
943
|
-
drawGraphOnly();
|
|
944
|
-
}
|
|
945
|
-
};
|
|
946
|
-
canvas.onclick = () => {
|
|
947
|
-
if (graphState.hover) inspectFile(graphState.hover.path);
|
|
948
|
-
};
|
|
949
|
-
}
|
|
1056
|
+
// Force simulation
|
|
1057
|
+
graphSim = d3.forceSimulation(nodes)
|
|
1058
|
+
.force('link', d3.forceLink(links).id(d => d.id).distance(d => 60 + 40 / Math.max(d.weight, 1)))
|
|
1059
|
+
.force('charge', d3.forceManyBody().strength(-100))
|
|
1060
|
+
.force('center', d3.forceCenter(w / 2, h / 2))
|
|
1061
|
+
.force('collision', d3.forceCollide().radius(d => nodeRadius(d) + 2))
|
|
1062
|
+
.alphaDecay(0.03)
|
|
1063
|
+
.on('tick', () => {
|
|
1064
|
+
graphLinkSel
|
|
1065
|
+
.attr('x1', d => d.source.x)
|
|
1066
|
+
.attr('y1', d => d.source.y)
|
|
1067
|
+
.attr('x2', d => d.target.x)
|
|
1068
|
+
.attr('y2', d => d.target.y);
|
|
1069
|
+
graphNodeSel.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
|
|
1070
|
+
});
|
|
950
1071
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1072
|
+
// Render legend
|
|
1073
|
+
renderGraphLegend();
|
|
1074
|
+
|
|
1075
|
+
// Update slider max
|
|
1076
|
+
const maxPR = Math.max(...nodes.map(n => n.pagerank || 0), 0.01);
|
|
1077
|
+
const slider = document.getElementById('graph-pr-slider');
|
|
1078
|
+
slider.max = 100;
|
|
1079
|
+
slider.value = 0;
|
|
1080
|
+
document.getElementById('graph-pr-val').textContent = '0.00';
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function dragStart(event, d) {
|
|
1084
|
+
if (!event.active) graphSim.alphaTarget(0.3).restart();
|
|
1085
|
+
d.fx = d.x;
|
|
1086
|
+
d.fy = d.y;
|
|
1087
|
+
}
|
|
1088
|
+
function dragging(event, d) {
|
|
1089
|
+
d.fx = event.x;
|
|
1090
|
+
d.fy = event.y;
|
|
1091
|
+
}
|
|
1092
|
+
function dragEnd(event, d) {
|
|
1093
|
+
if (!event.active) graphSim.alphaTarget(0);
|
|
1094
|
+
d.fx = null;
|
|
1095
|
+
d.fy = null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function graphApplyFilter() {
|
|
1099
|
+
if (!graphNodeSel || !graphLinkSel) return;
|
|
1100
|
+
const textFilter = (state.graphFilter || '').toLowerCase();
|
|
1101
|
+
const slider = document.getElementById('graph-pr-slider');
|
|
1102
|
+
const maxPR = state.graphData ? Math.max(...state.graphData.nodes.map(n => n.pagerank || 0), 0.01) : 1;
|
|
1103
|
+
const minPR = (parseInt(slider.value) / 100) * maxPR;
|
|
1104
|
+
document.getElementById('graph-pr-val').textContent = minPR.toFixed(2);
|
|
1105
|
+
|
|
1106
|
+
graphNodeSel.each(function(d) {
|
|
1107
|
+
const textMatch = !textFilter || d.path.toLowerCase().includes(textFilter);
|
|
1108
|
+
const prMatch = (d.pagerank || 0) >= minPR;
|
|
1109
|
+
const visible = textMatch && prMatch;
|
|
1110
|
+
d._visible = visible;
|
|
1111
|
+
d3.select(this).style('opacity', visible ? 1 : 0.08);
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
graphLinkSel.style('opacity', function(d) {
|
|
1115
|
+
const srcVis = d.source._visible !== false;
|
|
1116
|
+
const tgtVis = d.target._visible !== false;
|
|
1117
|
+
return (srcVis && tgtVis) ? edgeOpacity(d) : 0.02;
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
977
1120
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
ctx.beginPath();
|
|
985
|
-
ctx.arc(n.x, n.y, size, 0, Math.PI*2);
|
|
986
|
-
ctx.fillStyle = getLangColor(n.language);
|
|
987
|
-
if (graphState.hover === n) ctx.fillStyle = '#E85A2A';
|
|
988
|
-
ctx.fill();
|
|
989
|
-
if ((n.pagerank || 0) > 0.3 || graphState.hover === n) {
|
|
990
|
-
ctx.font = '11px "JetBrains Mono", monospace';
|
|
991
|
-
ctx.fillStyle = '#A39886';
|
|
992
|
-
ctx.fillText(n.path.split('/').pop(), n.x + size + 4, n.y + 3);
|
|
1121
|
+
function renderGraphLegend() {
|
|
1122
|
+
if (!state.graphData) return;
|
|
1123
|
+
const langs = new Map();
|
|
1124
|
+
for (const n of state.graphData.nodes) {
|
|
1125
|
+
if (n.language && !langs.has(n.language)) {
|
|
1126
|
+
langs.set(n.language, getLangColor(n.language));
|
|
993
1127
|
}
|
|
994
1128
|
}
|
|
995
|
-
|
|
1129
|
+
const el = document.getElementById('graph-legend');
|
|
1130
|
+
el.innerHTML = Array.from(langs.entries()).map(([lang, color]) =>
|
|
1131
|
+
'<div class="graph-legend-item"><div class="graph-legend-dot" style="background:' + color + '"></div><span>' + lang + '</span></div>'
|
|
1132
|
+
).join('') +
|
|
1133
|
+
'<div class="graph-legend-item" style="margin-top:4px;color:var(--text-3);font-size:9px;">' +
|
|
1134
|
+
(state.graphData.total || state.graphData.nodes.length) + ' total files' +
|
|
1135
|
+
(state.graphData.nodes.length < (state.graphData.total || 0) ? ' · showing top ' + state.graphData.nodes.length : '') +
|
|
1136
|
+
'</div>';
|
|
996
1137
|
}
|
|
997
1138
|
|
|
998
1139
|
function getLangColor(lang) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard-html.js","sourceRoot":"","sources":["../../../src/server/dashboard-html.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB;IAC9B,OAAO
|
|
1
|
+
{"version":3,"file":"dashboard-html.js","sourceRoot":"","sources":["../../../src/server/dashboard-html.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB;IAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAq6CD,CAAC;AACT,CAAC"}
|
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { log } from "../utils/logger.js";
|
|
6
6
|
import { getDashboardHTML } from "./dashboard-html.js";
|
|
7
|
+
import { getClustersJSON } from "./tools/clusters.js";
|
|
7
8
|
// Read the package version once at module load so the dashboard footer
|
|
8
9
|
// and any other surface can show what version is actually running.
|
|
9
10
|
// Was hardcoded as "v0.1.7" in the dashboard HTML until a dogfood
|
|
@@ -130,6 +131,30 @@ export function startHttpServer(indexer, port = 3847) {
|
|
|
130
131
|
}));
|
|
131
132
|
json(res, overview);
|
|
132
133
|
}
|
|
134
|
+
else if (url.pathname === "/api/graph") {
|
|
135
|
+
const limitParam = url.searchParams.get("limit");
|
|
136
|
+
const limit = limitParam ? parseInt(limitParam, 10) : 100;
|
|
137
|
+
const allFiles = indexer.fileStore.getAll(); // already sorted by pagerank DESC
|
|
138
|
+
const allEdges = indexer.graphStore.getAll();
|
|
139
|
+
const files = limit > 0 ? allFiles.slice(0, limit) : allFiles;
|
|
140
|
+
const fileIdSet = new Set(files.map(f => f.id));
|
|
141
|
+
const edges = allEdges.filter(e => fileIdSet.has(e.source_file_id) && fileIdSet.has(e.target_file_id));
|
|
142
|
+
json(res, {
|
|
143
|
+
nodes: files.map(f => ({
|
|
144
|
+
id: f.id,
|
|
145
|
+
path: f.path,
|
|
146
|
+
language: f.language,
|
|
147
|
+
pagerank: f.pagerank,
|
|
148
|
+
size_bytes: f.size_bytes,
|
|
149
|
+
})),
|
|
150
|
+
edges: edges.map(e => ({
|
|
151
|
+
source: e.source_file_id,
|
|
152
|
+
target: e.target_file_id,
|
|
153
|
+
weight: e.reference_count,
|
|
154
|
+
})),
|
|
155
|
+
total: allFiles.length,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
133
158
|
else if (url.pathname === "/api/deps") {
|
|
134
159
|
const files = indexer.fileStore.getAll();
|
|
135
160
|
const fileMap = new Map(files.map(f => [f.id, f.path]));
|
|
@@ -157,6 +182,10 @@ export function startHttpServer(indexer, port = 3847) {
|
|
|
157
182
|
edges,
|
|
158
183
|
});
|
|
159
184
|
}
|
|
185
|
+
else if (url.pathname === "/api/clusters") {
|
|
186
|
+
const clusters = getClustersJSON(indexer);
|
|
187
|
+
json(res, clusters);
|
|
188
|
+
}
|
|
160
189
|
else if (url.pathname === "/api/search") {
|
|
161
190
|
const q = url.searchParams.get("q");
|
|
162
191
|
if (!q) {
|