clawvault 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +352 -20
- package/bin/clawvault.js +8 -2
- package/bin/command-registration.test.js +3 -1
- package/bin/command-runtime.js +9 -1
- package/bin/register-core-commands.js +23 -10
- package/bin/register-maintenance-commands.js +39 -3
- package/bin/register-query-commands.js +58 -29
- package/bin/register-task-commands.js +18 -1
- package/bin/register-task-commands.test.js +16 -0
- package/bin/register-vault-operations-commands.js +29 -1
- package/bin/register-workgraph-commands.js +1368 -0
- package/dashboard/lib/graph-diff.js +104 -0
- package/dashboard/lib/graph-diff.test.js +75 -0
- package/dashboard/lib/vault-parser.js +556 -0
- package/dashboard/lib/vault-parser.test.js +254 -0
- package/dashboard/public/app.js +796 -0
- package/dashboard/public/index.html +52 -0
- package/dashboard/public/styles.css +221 -0
- package/dashboard/server.js +374 -0
- package/dist/{chunk-F2JEUD4J.js → chunk-23YDQ3QU.js} +6 -8
- package/dist/{chunk-C7OK5WKP.js → chunk-2JQ3O2YL.js} +4 -4
- package/dist/{chunk-VR5NE7PZ.js → chunk-2RAZ4ZFE.js} +1 -1
- package/dist/chunk-2ZDO52B4.js +52 -0
- package/dist/{chunk-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
- package/dist/chunk-33VSQP4J.js +37 -0
- package/dist/chunk-4BQTQMJP.js +93 -0
- package/dist/{chunk-GUKMRGM7.js → chunk-4OXMU5S2.js} +1 -1
- package/dist/{chunk-62YTUT6J.js → chunk-4PY655YM.js} +15 -3
- package/dist/chunk-6FH3IULF.js +352 -0
- package/dist/{chunk-3NSBOUT3.js → chunk-77Q5CSPJ.js} +404 -80
- package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
- package/dist/chunk-BSJ6RIT7.js +447 -0
- package/dist/chunk-BUEW6IIK.js +364 -0
- package/dist/{chunk-WGRQ6HDV.js → chunk-CLJTREDS.js} +74 -14
- package/dist/chunk-EK6S23ZB.js +469 -0
- package/dist/{chunk-LNJA2UGL.js → chunk-ESFLMDRB.js} +9 -86
- package/dist/{chunk-H34S76MB.js → chunk-ESVS6K2B.js} +6 -6
- package/dist/{chunk-WAZ3NLWL.js → chunk-F55HGNU4.js} +0 -47
- package/dist/{chunk-QK3UCXWL.js → chunk-FHFUXL6G.js} +2 -2
- package/dist/{chunk-YKTA5JOJ.js → chunk-GAOWA7GR.js} +212 -46
- package/dist/chunk-GGA32J2R.js +784 -0
- package/dist/chunk-GNJL4YGR.js +79 -0
- package/dist/chunk-MDIH26GC.js +183 -0
- package/dist/{chunk-LYHGEHXG.js → chunk-MFAWT5O5.js} +0 -1
- package/dist/chunk-MM6QGW3P.js +207 -0
- package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
- package/dist/chunk-NCKFNBHJ.js +257 -0
- package/dist/{chunk-QBLMXKF2.js → chunk-OIWVQYQF.js} +1 -1
- package/dist/{chunk-42MXU7A6.js → chunk-P62WHA27.js} +58 -47
- package/dist/chunk-PBACDKKP.js +66 -0
- package/dist/{chunk-VGLOTGAS.js → chunk-QSHD36LH.js} +2 -2
- package/dist/{chunk-OZ7RIXTO.js → chunk-QSRRMEYM.js} +2 -2
- package/dist/chunk-QVEERJSP.js +152 -0
- package/dist/{chunk-N2AXRYLC.js → chunk-QWQ3TIKS.js} +1 -1
- package/dist/{chunk-3DHXQHYG.js → chunk-R2MIW5G7.js} +1 -1
- package/dist/{chunk-SJSFRIYS.js → chunk-SLXOR3CC.js} +2 -2
- package/dist/chunk-SS4B7P7V.js +99 -0
- package/dist/{chunk-JY6FYXIT.js → chunk-STCQGCEQ.js} +6 -11
- package/dist/chunk-U4O6C46S.js +154 -0
- package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
- package/dist/chunk-VSL7KY3M.js +189 -0
- package/dist/{chunk-U55BGUAU.js → chunk-W4SPAEE7.js} +6 -6
- package/dist/chunk-WMGIIABP.js +15 -0
- package/dist/{chunk-3D6BCTP6.js → chunk-X3SPPUFG.js} +51 -39
- package/dist/{chunk-THRJVD4L.js → chunk-Y6VJKXGL.js} +1 -1
- package/dist/{chunk-ZVVFWOLW.js → chunk-ZN54U2OZ.js} +123 -10
- package/dist/cli/index.js +32 -25
- package/dist/commands/archive.js +3 -3
- package/dist/commands/backlog.js +3 -3
- package/dist/commands/blocked.js +3 -3
- package/dist/commands/canvas.d.ts +15 -0
- package/dist/commands/canvas.js +200 -0
- package/dist/commands/checkpoint.js +2 -2
- package/dist/commands/compat.js +2 -2
- package/dist/commands/context.js +8 -6
- package/dist/commands/doctor.d.ts +11 -7
- package/dist/commands/doctor.js +18 -16
- package/dist/commands/embed.js +5 -6
- package/dist/commands/entities.js +2 -2
- package/dist/commands/graph.js +4 -4
- package/dist/commands/inject.d.ts +1 -1
- package/dist/commands/inject.js +5 -6
- package/dist/commands/kanban.js +4 -4
- package/dist/commands/link.js +5 -5
- package/dist/commands/migrate-observations.js +4 -4
- package/dist/commands/observe.d.ts +0 -1
- package/dist/commands/observe.js +14 -13
- package/dist/commands/project.js +5 -5
- package/dist/commands/rebuild-embeddings.d.ts +21 -0
- package/dist/commands/rebuild-embeddings.js +91 -0
- package/dist/commands/rebuild.js +12 -11
- package/dist/commands/recover.js +3 -3
- package/dist/commands/reflect.js +6 -7
- package/dist/commands/repair-session.js +1 -1
- package/dist/commands/replay.js +14 -14
- package/dist/commands/session-recap.js +1 -1
- package/dist/commands/setup.d.ts +2 -90
- package/dist/commands/setup.js +3 -21
- package/dist/commands/shell-init.js +1 -1
- package/dist/commands/sleep.d.ts +1 -1
- package/dist/commands/sleep.js +20 -19
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +57 -35
- package/dist/commands/sync-bd.d.ts +10 -0
- package/dist/commands/sync-bd.js +10 -0
- package/dist/commands/tailscale.js +3 -3
- package/dist/commands/task.js +4 -4
- package/dist/commands/template.js +2 -2
- package/dist/commands/wake.d.ts +1 -1
- package/dist/commands/wake.js +11 -10
- package/dist/commands/workgraph.d.ts +124 -0
- package/dist/commands/workgraph.js +38 -0
- package/dist/index.d.ts +337 -191
- package/dist/index.js +387 -118
- package/dist/{inject-Bzi5E-By.d.cts → inject-DYUrDqQO.d.ts} +3 -3
- package/dist/ledger-B7g7jhqG.d.ts +44 -0
- package/dist/lib/auto-linker.js +2 -2
- package/dist/lib/canvas-layout.d.ts +100 -16
- package/dist/lib/canvas-layout.js +21 -78
- package/dist/lib/config.d.ts +27 -3
- package/dist/lib/config.js +4 -2
- package/dist/lib/entity-index.js +1 -1
- package/dist/lib/project-utils.js +4 -4
- package/dist/lib/session-repair.js +1 -1
- package/dist/lib/session-utils.js +1 -1
- package/dist/lib/tailscale.js +1 -1
- package/dist/lib/task-utils.js +3 -3
- package/dist/lib/template-engine.js +1 -1
- package/dist/lib/webdav.js +1 -1
- package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
- package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
- package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
- package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
- package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
- package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
- package/dist/openclaw-plugin.d.ts +8 -0
- package/dist/openclaw-plugin.js +14 -0
- package/dist/registry-BR4326o0.d.ts +30 -0
- package/dist/store-CA-6sKCJ.d.ts +34 -0
- package/dist/thread-B9LhXNU0.d.ts +41 -0
- package/dist/transformers.node-A2ZRORSQ.js +46775 -0
- package/dist/{types-Y2_Um2Ls.d.cts → types-BbWJoC1c.d.ts} +1 -44
- package/dist/workgraph/index.d.ts +5 -0
- package/dist/workgraph/index.js +23 -0
- package/dist/workgraph/ledger.d.ts +2 -0
- package/dist/workgraph/ledger.js +25 -0
- package/dist/workgraph/registry.d.ts +2 -0
- package/dist/workgraph/registry.js +19 -0
- package/dist/workgraph/store.d.ts +2 -0
- package/dist/workgraph/store.js +25 -0
- package/dist/workgraph/thread.d.ts +2 -0
- package/dist/workgraph/thread.js +25 -0
- package/dist/workgraph/types.d.ts +54 -0
- package/dist/workgraph/types.js +7 -0
- package/hooks/clawvault/HOOK.md +34 -4
- package/hooks/clawvault/handler.js +760 -78
- package/hooks/clawvault/handler.test.js +235 -79
- package/hooks/clawvault/openclaw.plugin.json +72 -0
- package/openclaw.plugin.json +65 -38
- package/package.json +15 -18
- package/dist/chunk-3RG5ZIWI.js +0 -10
- package/dist/chunk-6U6MK36V.js +0 -205
- package/dist/chunk-7R7O6STJ.js +0 -88
- package/dist/chunk-CMB7UL7C.js +0 -327
- package/dist/chunk-DEFFDRVP.js +0 -938
- package/dist/chunk-E7MFQB6D.js +0 -163
- package/dist/chunk-GAJV4IGR.js +0 -82
- package/dist/chunk-GQSLDZTS.js +0 -560
- package/dist/chunk-K234IDRJ.js +0 -1073
- package/dist/chunk-MFM6K7PU.js +0 -374
- package/dist/chunk-MXSSG3QU.js +0 -42
- package/dist/chunk-PAH27GSN.js +0 -108
- package/dist/cli/index.cjs +0 -10033
- package/dist/cli/index.d.cts +0 -5
- package/dist/commands/archive.cjs +0 -287
- package/dist/commands/archive.d.cts +0 -11
- package/dist/commands/backlog.cjs +0 -721
- package/dist/commands/backlog.d.cts +0 -53
- package/dist/commands/blocked.cjs +0 -204
- package/dist/commands/blocked.d.cts +0 -26
- package/dist/commands/checkpoint.cjs +0 -244
- package/dist/commands/checkpoint.d.cts +0 -41
- package/dist/commands/compat.cjs +0 -369
- package/dist/commands/compat.d.cts +0 -28
- package/dist/commands/context.cjs +0 -2989
- package/dist/commands/context.d.cts +0 -2
- package/dist/commands/doctor.cjs +0 -3062
- package/dist/commands/doctor.d.cts +0 -21
- package/dist/commands/embed.cjs +0 -232
- package/dist/commands/embed.d.cts +0 -17
- package/dist/commands/entities.cjs +0 -141
- package/dist/commands/entities.d.cts +0 -7
- package/dist/commands/graph.cjs +0 -501
- package/dist/commands/graph.d.cts +0 -21
- package/dist/commands/inject.cjs +0 -1636
- package/dist/commands/inject.d.cts +0 -2
- package/dist/commands/kanban.cjs +0 -884
- package/dist/commands/kanban.d.cts +0 -63
- package/dist/commands/link.cjs +0 -965
- package/dist/commands/link.d.cts +0 -11
- package/dist/commands/migrate-observations.cjs +0 -362
- package/dist/commands/migrate-observations.d.cts +0 -19
- package/dist/commands/observe.cjs +0 -4099
- package/dist/commands/observe.d.cts +0 -23
- package/dist/commands/project.cjs +0 -1341
- package/dist/commands/project.d.cts +0 -85
- package/dist/commands/rebuild.cjs +0 -3136
- package/dist/commands/rebuild.d.cts +0 -11
- package/dist/commands/recover.cjs +0 -361
- package/dist/commands/recover.d.cts +0 -38
- package/dist/commands/reflect.cjs +0 -1008
- package/dist/commands/reflect.d.cts +0 -11
- package/dist/commands/repair-session.cjs +0 -457
- package/dist/commands/repair-session.d.cts +0 -38
- package/dist/commands/replay.cjs +0 -4103
- package/dist/commands/replay.d.cts +0 -16
- package/dist/commands/session-recap.cjs +0 -353
- package/dist/commands/session-recap.d.cts +0 -27
- package/dist/commands/setup.cjs +0 -1345
- package/dist/commands/setup.d.cts +0 -100
- package/dist/commands/shell-init.cjs +0 -75
- package/dist/commands/shell-init.d.cts +0 -7
- package/dist/commands/sleep.cjs +0 -6028
- package/dist/commands/sleep.d.cts +0 -36
- package/dist/commands/status.cjs +0 -2736
- package/dist/commands/status.d.cts +0 -52
- package/dist/commands/tailscale.cjs +0 -1532
- package/dist/commands/tailscale.d.cts +0 -52
- package/dist/commands/task.cjs +0 -1236
- package/dist/commands/task.d.cts +0 -97
- package/dist/commands/template.cjs +0 -457
- package/dist/commands/template.d.cts +0 -36
- package/dist/commands/wake.cjs +0 -2626
- package/dist/commands/wake.d.cts +0 -22
- package/dist/context-BUGaWpyL.d.cts +0 -46
- package/dist/index.cjs +0 -14526
- package/dist/index.d.cts +0 -858
- package/dist/inject-Bzi5E-By.d.ts +0 -137
- package/dist/lib/auto-linker.cjs +0 -176
- package/dist/lib/auto-linker.d.cts +0 -26
- package/dist/lib/canvas-layout.cjs +0 -136
- package/dist/lib/canvas-layout.d.cts +0 -31
- package/dist/lib/config.cjs +0 -78
- package/dist/lib/config.d.cts +0 -11
- package/dist/lib/entity-index.cjs +0 -84
- package/dist/lib/entity-index.d.cts +0 -26
- package/dist/lib/project-utils.cjs +0 -864
- package/dist/lib/project-utils.d.cts +0 -97
- package/dist/lib/session-repair.cjs +0 -239
- package/dist/lib/session-repair.d.cts +0 -110
- package/dist/lib/session-utils.cjs +0 -209
- package/dist/lib/session-utils.d.cts +0 -63
- package/dist/lib/tailscale.cjs +0 -1183
- package/dist/lib/tailscale.d.cts +0 -225
- package/dist/lib/task-utils.cjs +0 -1137
- package/dist/lib/task-utils.d.cts +0 -208
- package/dist/lib/template-engine.cjs +0 -47
- package/dist/lib/template-engine.d.cts +0 -11
- package/dist/lib/webdav.cjs +0 -568
- package/dist/lib/webdav.d.cts +0 -109
- package/dist/plugin/index.cjs +0 -1907
- package/dist/plugin/index.d.cts +0 -36
- package/dist/plugin/index.d.ts +0 -36
- package/dist/plugin/index.js +0 -572
- package/dist/plugin/inject.cjs +0 -356
- package/dist/plugin/inject.d.cts +0 -54
- package/dist/plugin/inject.d.ts +0 -54
- package/dist/plugin/inject.js +0 -17
- package/dist/plugin/observe.cjs +0 -631
- package/dist/plugin/observe.d.cts +0 -39
- package/dist/plugin/observe.d.ts +0 -39
- package/dist/plugin/observe.js +0 -18
- package/dist/plugin/templates.cjs +0 -593
- package/dist/plugin/templates.d.cts +0 -52
- package/dist/plugin/templates.d.ts +0 -52
- package/dist/plugin/templates.js +0 -25
- package/dist/plugin/types.cjs +0 -18
- package/dist/plugin/types.d.cts +0 -209
- package/dist/plugin/types.d.ts +0 -209
- package/dist/plugin/types.js +0 -0
- package/dist/plugin/vault.cjs +0 -927
- package/dist/plugin/vault.d.cts +0 -68
- package/dist/plugin/vault.d.ts +0 -68
- package/dist/plugin/vault.js +0 -22
- package/dist/types-Y2_Um2Ls.d.ts +0 -205
- package/templates/memory-event.md +0 -67
- package/templates/party.md +0 -63
- package/templates/primitive-registry.yaml +0 -551
- package/templates/run.md +0 -68
- package/templates/trigger.md +0 -68
- package/templates/workspace.md +0 -50
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
const CATEGORY_COLORS = {
|
|
2
|
+
decisions: '#ff8b6a',
|
|
3
|
+
lessons: '#7af5e9',
|
|
4
|
+
people: '#ff8ea9',
|
|
5
|
+
projects: '#9ec6ff',
|
|
6
|
+
commitments: '#ffe18d',
|
|
7
|
+
research: '#9bf7bd',
|
|
8
|
+
unresolved: '#ffb363',
|
|
9
|
+
root: '#b4bcd1',
|
|
10
|
+
default: '#9dadc5'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DIMMED_NODE_COLOR = 'rgba(67, 85, 108, 0.45)';
|
|
14
|
+
const DIMMED_LINK_COLOR = 'rgba(117, 138, 166, 0.16)';
|
|
15
|
+
const NORMAL_LINK_COLOR = 'rgba(167, 189, 214, 0.34)';
|
|
16
|
+
const HIGHLIGHT_LINK_COLOR = 'rgba(239, 247, 255, 0.92)';
|
|
17
|
+
|
|
18
|
+
const FILTER_DEBOUNCE_MS = 120;
|
|
19
|
+
const GRAPH_UPDATE_THROTTLE_MS = 1_000;
|
|
20
|
+
|
|
21
|
+
const state = {
|
|
22
|
+
allNodeById: new Map(),
|
|
23
|
+
allEdgeByKey: new Map(),
|
|
24
|
+
neighborsByNodeId: new Map(),
|
|
25
|
+
linksByNodeId: new Map(),
|
|
26
|
+
searchTerm: '',
|
|
27
|
+
category: 'all',
|
|
28
|
+
tag: 'all',
|
|
29
|
+
nodeType: 'all',
|
|
30
|
+
visibleNodeIds: new Set(),
|
|
31
|
+
hoveredNodeId: null,
|
|
32
|
+
selectedNodeId: null,
|
|
33
|
+
highlightedNodeIds: new Set(),
|
|
34
|
+
highlightedEdgeKeys: new Set(),
|
|
35
|
+
stats: null,
|
|
36
|
+
wsVersion: 0,
|
|
37
|
+
wsConnected: false,
|
|
38
|
+
tvMode: false,
|
|
39
|
+
lastInteractionAt: performance.now(),
|
|
40
|
+
filterDebounce: null,
|
|
41
|
+
graphDataThrottleTimer: null,
|
|
42
|
+
pendingGraphData: null,
|
|
43
|
+
lastGraphDataApplyAt: 0
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const graphElement = document.querySelector('#graph');
|
|
47
|
+
const detailsElement = document.querySelector('#node-details');
|
|
48
|
+
const statsElement = document.querySelector('#stats');
|
|
49
|
+
const searchElement = document.querySelector('#search');
|
|
50
|
+
const categoryFilterElement = document.querySelector('#category-filter');
|
|
51
|
+
const tagFilterElement = document.querySelector('#tag-filter');
|
|
52
|
+
const nodeTypeFilterElement = document.querySelector('#node-type-filter');
|
|
53
|
+
const refreshButtonElement = document.querySelector('#refresh');
|
|
54
|
+
const tvModeButtonElement = document.querySelector('#tv-mode');
|
|
55
|
+
const realtimeStatusElement = document.querySelector('#realtime-status');
|
|
56
|
+
|
|
57
|
+
if (typeof window.ForceGraph !== 'function') {
|
|
58
|
+
statsElement.textContent = 'ForceGraph failed to load.';
|
|
59
|
+
throw new Error('force-graph library unavailable');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const graph = window
|
|
63
|
+
.ForceGraph()(graphElement)
|
|
64
|
+
.backgroundColor('#070a10')
|
|
65
|
+
.nodeId('id')
|
|
66
|
+
.linkSource('source')
|
|
67
|
+
.linkTarget('target')
|
|
68
|
+
.nodeRelSize(4)
|
|
69
|
+
.d3AlphaDecay(0.018)
|
|
70
|
+
.d3VelocityDecay(0.24)
|
|
71
|
+
.cooldownTicks(90)
|
|
72
|
+
.linkColor((link) => getLinkColor(link))
|
|
73
|
+
.linkWidth((link) => getLinkWidth(link))
|
|
74
|
+
.linkDirectionalParticles(0)
|
|
75
|
+
.nodeCanvasObject((node, ctx, globalScale) => renderNode(node, ctx, globalScale))
|
|
76
|
+
.nodePointerAreaPaint((node, color, ctx) => {
|
|
77
|
+
ctx.fillStyle = color;
|
|
78
|
+
ctx.beginPath();
|
|
79
|
+
ctx.arc(node.x, node.y, getNodeRadius(node) + 3, 0, Math.PI * 2, false);
|
|
80
|
+
ctx.fill();
|
|
81
|
+
})
|
|
82
|
+
.onNodeHover((node) => {
|
|
83
|
+
const nextHoveredNodeId = node?.id ?? null;
|
|
84
|
+
if (nextHoveredNodeId === state.hoveredNodeId) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
state.hoveredNodeId = nextHoveredNodeId;
|
|
88
|
+
markInteraction();
|
|
89
|
+
syncHighlights();
|
|
90
|
+
})
|
|
91
|
+
.onNodeClick((node) => {
|
|
92
|
+
markInteraction();
|
|
93
|
+
selectNode(node);
|
|
94
|
+
})
|
|
95
|
+
.onBackgroundClick(() => {
|
|
96
|
+
markInteraction();
|
|
97
|
+
state.selectedNodeId = null;
|
|
98
|
+
syncHighlights();
|
|
99
|
+
renderEmptyDetails();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
resizeGraphToContainer();
|
|
103
|
+
window.addEventListener('resize', resizeGraphToContainer);
|
|
104
|
+
|
|
105
|
+
searchElement.addEventListener('input', (event) => {
|
|
106
|
+
state.searchTerm = String(event.target.value ?? '').trim().toLowerCase();
|
|
107
|
+
markInteraction();
|
|
108
|
+
scheduleFilterApply();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
categoryFilterElement.addEventListener('change', (event) => {
|
|
112
|
+
state.category = String(event.target.value ?? 'all');
|
|
113
|
+
markInteraction();
|
|
114
|
+
applyFiltersAndRender({ shouldLazyLoad: false });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
tagFilterElement.addEventListener('change', (event) => {
|
|
118
|
+
state.tag = String(event.target.value ?? 'all');
|
|
119
|
+
markInteraction();
|
|
120
|
+
applyFiltersAndRender({ shouldLazyLoad: false });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
nodeTypeFilterElement.addEventListener('change', (event) => {
|
|
124
|
+
state.nodeType = String(event.target.value ?? 'all');
|
|
125
|
+
markInteraction();
|
|
126
|
+
applyFiltersAndRender({ shouldLazyLoad: false });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
refreshButtonElement.addEventListener('click', async () => {
|
|
130
|
+
markInteraction();
|
|
131
|
+
await fetchSnapshot({ refresh: true });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
tvModeButtonElement.addEventListener('click', () => {
|
|
135
|
+
markInteraction();
|
|
136
|
+
setTvMode(!state.tvMode, { updateQuery: true, useFullscreen: true });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
detailsElement.addEventListener('click', (event) => {
|
|
140
|
+
const target = event.target;
|
|
141
|
+
if (!(target instanceof HTMLElement)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const linkedNodeId = target.dataset.nodeId;
|
|
145
|
+
if (!linkedNodeId) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
const node = state.allNodeById.get(linkedNodeId);
|
|
150
|
+
if (node) {
|
|
151
|
+
markInteraction();
|
|
152
|
+
selectNode(node);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
window.addEventListener('keydown', (event) => {
|
|
157
|
+
if (event.key.toLowerCase() === 't') {
|
|
158
|
+
setTvMode(!state.tvMode, { updateQuery: true, useFullscreen: true });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
for (const eventName of ['pointermove', 'wheel', 'keydown']) {
|
|
163
|
+
window.addEventListener(eventName, () => {
|
|
164
|
+
markInteraction();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const initialParams = new URLSearchParams(window.location.search);
|
|
169
|
+
if (initialParams.get('tv') === '1') {
|
|
170
|
+
setTvMode(true, { updateQuery: false, useFullscreen: false });
|
|
171
|
+
}
|
|
172
|
+
if (initialParams.get('webgl') === '1') {
|
|
173
|
+
enableWebglRendererIfSupported();
|
|
174
|
+
}
|
|
175
|
+
if (initialParams.get('realtime') === '1') {
|
|
176
|
+
startRealtime();
|
|
177
|
+
} else {
|
|
178
|
+
setRealtimeStatus({ text: 'Realtime: disabled (stability mode)', level: 'warn' });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
void fetchSnapshot();
|
|
182
|
+
|
|
183
|
+
function resizeGraphToContainer() {
|
|
184
|
+
graph.width(graphElement.clientWidth);
|
|
185
|
+
graph.height(graphElement.clientHeight);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function fetchSnapshot({ refresh = false } = {}) {
|
|
189
|
+
refreshButtonElement.disabled = true;
|
|
190
|
+
statsElement.textContent = 'Loading graph...';
|
|
191
|
+
try {
|
|
192
|
+
const response = await fetch(refresh ? '/api/graph?refresh=1' : '/api/graph');
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
throw new Error(`HTTP ${response.status}`);
|
|
195
|
+
}
|
|
196
|
+
const payload = await response.json();
|
|
197
|
+
applySnapshot(payload, { shouldLazyLoad: true, shouldFit: true });
|
|
198
|
+
} catch (error) {
|
|
199
|
+
statsElement.textContent = `Failed to load graph: ${error instanceof Error ? error.message : String(error)}`;
|
|
200
|
+
} finally {
|
|
201
|
+
refreshButtonElement.disabled = false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function applySnapshot(payload, { shouldLazyLoad, shouldFit }) {
|
|
206
|
+
const nextNodes = Array.isArray(payload?.nodes) ? payload.nodes : [];
|
|
207
|
+
const nextEdges = Array.isArray(payload?.edges) ? payload.edges : [];
|
|
208
|
+
|
|
209
|
+
state.allNodeById = new Map();
|
|
210
|
+
for (const node of nextNodes) {
|
|
211
|
+
state.allNodeById.set(node.id, { ...node });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
state.allEdgeByKey = new Map();
|
|
215
|
+
for (const edge of nextEdges) {
|
|
216
|
+
const sourceId = String(edge.source);
|
|
217
|
+
const targetId = String(edge.target);
|
|
218
|
+
const edgeType = String(edge.type ?? '');
|
|
219
|
+
const edgeLabel = String(edge.label ?? '');
|
|
220
|
+
const edgeKey = toEdgeKey(sourceId, targetId, edgeType, edgeLabel);
|
|
221
|
+
state.allEdgeByKey.set(edgeKey, {
|
|
222
|
+
key: edgeKey,
|
|
223
|
+
source: sourceId,
|
|
224
|
+
target: targetId,
|
|
225
|
+
type: edgeType,
|
|
226
|
+
label: edgeLabel
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
state.stats = payload?.stats ?? null;
|
|
231
|
+
rebuildConnectionIndexes();
|
|
232
|
+
populateFilters();
|
|
233
|
+
applyFiltersAndRender({ shouldLazyLoad });
|
|
234
|
+
syncHighlights();
|
|
235
|
+
if (shouldFit) {
|
|
236
|
+
graph.zoomToFit(700, 100);
|
|
237
|
+
}
|
|
238
|
+
if (state.selectedNodeId) {
|
|
239
|
+
const selectedNode = state.allNodeById.get(state.selectedNodeId);
|
|
240
|
+
if (selectedNode) {
|
|
241
|
+
renderDetails(selectedNode);
|
|
242
|
+
} else {
|
|
243
|
+
state.selectedNodeId = null;
|
|
244
|
+
renderEmptyDetails();
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
renderEmptyDetails();
|
|
248
|
+
}
|
|
249
|
+
updateStats();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function applyPatch(payload) {
|
|
253
|
+
const addedNodes = Array.isArray(payload?.addedNodes) ? payload.addedNodes : [];
|
|
254
|
+
const updatedNodes = Array.isArray(payload?.updatedNodes) ? payload.updatedNodes : [];
|
|
255
|
+
const removedNodeIds = Array.isArray(payload?.removedNodeIds) ? payload.removedNodeIds : [];
|
|
256
|
+
const addedEdges = Array.isArray(payload?.addedEdges) ? payload.addedEdges : [];
|
|
257
|
+
const removedEdges = Array.isArray(payload?.removedEdges) ? payload.removedEdges : [];
|
|
258
|
+
|
|
259
|
+
for (const nodeId of removedNodeIds) {
|
|
260
|
+
state.allNodeById.delete(nodeId);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const node of addedNodes) {
|
|
264
|
+
state.allNodeById.set(node.id, { ...node });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const patchNode of updatedNodes) {
|
|
268
|
+
const existingNode = state.allNodeById.get(patchNode.id);
|
|
269
|
+
if (existingNode) {
|
|
270
|
+
Object.assign(existingNode, patchNode);
|
|
271
|
+
} else {
|
|
272
|
+
state.allNodeById.set(patchNode.id, { ...patchNode });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const edge of removedEdges) {
|
|
277
|
+
const sourceId = String(edge.source);
|
|
278
|
+
const targetId = String(edge.target);
|
|
279
|
+
const edgeType = String(edge.type ?? '');
|
|
280
|
+
const edgeLabel = String(edge.label ?? '');
|
|
281
|
+
state.allEdgeByKey.delete(toEdgeKey(sourceId, targetId, edgeType, edgeLabel));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const edge of addedEdges) {
|
|
285
|
+
const sourceId = String(edge.source);
|
|
286
|
+
const targetId = String(edge.target);
|
|
287
|
+
const edgeType = String(edge.type ?? '');
|
|
288
|
+
const edgeLabel = String(edge.label ?? '');
|
|
289
|
+
const key = toEdgeKey(sourceId, targetId, edgeType, edgeLabel);
|
|
290
|
+
state.allEdgeByKey.set(key, { key, source: sourceId, target: targetId, type: edgeType, label: edgeLabel });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (payload?.stats) {
|
|
294
|
+
state.stats = payload.stats;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (removedNodeIds.includes(state.selectedNodeId)) {
|
|
298
|
+
state.selectedNodeId = null;
|
|
299
|
+
renderEmptyDetails();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
rebuildConnectionIndexes();
|
|
303
|
+
populateFilters();
|
|
304
|
+
applyFiltersAndRender({ shouldLazyLoad: false });
|
|
305
|
+
syncHighlights();
|
|
306
|
+
const selectedNode = state.selectedNodeId ? state.allNodeById.get(state.selectedNodeId) : null;
|
|
307
|
+
if (selectedNode) {
|
|
308
|
+
renderDetails(selectedNode);
|
|
309
|
+
}
|
|
310
|
+
updateStats();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function rebuildConnectionIndexes() {
|
|
314
|
+
state.neighborsByNodeId = new Map();
|
|
315
|
+
state.linksByNodeId = new Map();
|
|
316
|
+
|
|
317
|
+
for (const node of state.allNodeById.values()) {
|
|
318
|
+
state.neighborsByNodeId.set(node.id, new Set());
|
|
319
|
+
state.linksByNodeId.set(node.id, new Set());
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const edge of state.allEdgeByKey.values()) {
|
|
323
|
+
state.neighborsByNodeId.get(edge.source)?.add(edge.target);
|
|
324
|
+
state.neighborsByNodeId.get(edge.target)?.add(edge.source);
|
|
325
|
+
state.linksByNodeId.get(edge.source)?.add(edge.key);
|
|
326
|
+
state.linksByNodeId.get(edge.target)?.add(edge.key);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function applyFiltersAndRender({ shouldLazyLoad }) {
|
|
331
|
+
const filteredNodes = [];
|
|
332
|
+
for (const node of state.allNodeById.values()) {
|
|
333
|
+
if (isNodeVisible(node)) {
|
|
334
|
+
filteredNodes.push(node);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const visibleNodeIds = new Set(filteredNodes.map((node) => node.id));
|
|
339
|
+
const filteredLinks = [];
|
|
340
|
+
for (const edge of state.allEdgeByKey.values()) {
|
|
341
|
+
if (visibleNodeIds.has(edge.source) && visibleNodeIds.has(edge.target)) {
|
|
342
|
+
filteredLinks.push({
|
|
343
|
+
source: edge.source,
|
|
344
|
+
target: edge.target,
|
|
345
|
+
type: edge.type,
|
|
346
|
+
label: edge.label,
|
|
347
|
+
_key: edge.key
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
state.visibleNodeIds = visibleNodeIds;
|
|
353
|
+
renderGraph(filteredNodes, filteredLinks, { shouldLazyLoad });
|
|
354
|
+
updateStats();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function renderGraph(nodes, links, { shouldLazyLoad }) {
|
|
358
|
+
void shouldLazyLoad;
|
|
359
|
+
queueGraphDataUpdate(nodes, links);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function scheduleFilterApply() {
|
|
363
|
+
if (state.filterDebounce) {
|
|
364
|
+
clearTimeout(state.filterDebounce);
|
|
365
|
+
}
|
|
366
|
+
state.filterDebounce = setTimeout(() => {
|
|
367
|
+
state.filterDebounce = null;
|
|
368
|
+
applyFiltersAndRender({ shouldLazyLoad: false });
|
|
369
|
+
}, FILTER_DEBOUNCE_MS);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function syncHighlights() {
|
|
373
|
+
const nextHighlightedNodeIds = new Set();
|
|
374
|
+
const nextHighlightedEdgeKeys = new Set();
|
|
375
|
+
const focusNodeId = state.selectedNodeId ?? state.hoveredNodeId;
|
|
376
|
+
if (!focusNodeId || !state.visibleNodeIds.has(focusNodeId)) {
|
|
377
|
+
if (state.highlightedNodeIds.size === 0 && state.highlightedEdgeKeys.size === 0) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
state.highlightedNodeIds = nextHighlightedNodeIds;
|
|
381
|
+
state.highlightedEdgeKeys = nextHighlightedEdgeKeys;
|
|
382
|
+
graph.refresh();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
nextHighlightedNodeIds.add(focusNodeId);
|
|
387
|
+
for (const neighborNodeId of state.neighborsByNodeId.get(focusNodeId) ?? []) {
|
|
388
|
+
if (state.visibleNodeIds.has(neighborNodeId)) {
|
|
389
|
+
nextHighlightedNodeIds.add(neighborNodeId);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const edgeKey of state.linksByNodeId.get(focusNodeId) ?? []) {
|
|
393
|
+
nextHighlightedEdgeKeys.add(edgeKey);
|
|
394
|
+
}
|
|
395
|
+
if (
|
|
396
|
+
areSetsEqual(state.highlightedNodeIds, nextHighlightedNodeIds) &&
|
|
397
|
+
areSetsEqual(state.highlightedEdgeKeys, nextHighlightedEdgeKeys)
|
|
398
|
+
) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
state.highlightedNodeIds = nextHighlightedNodeIds;
|
|
402
|
+
state.highlightedEdgeKeys = nextHighlightedEdgeKeys;
|
|
403
|
+
graph.refresh();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function selectNode(node) {
|
|
407
|
+
state.selectedNodeId = node.id;
|
|
408
|
+
syncHighlights();
|
|
409
|
+
renderDetails(node);
|
|
410
|
+
focusNode(node, { zoom: state.tvMode ? 2.4 : 3.9, durationMs: 520 });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function focusNode(node, { zoom, durationMs }) {
|
|
414
|
+
graph.centerAt(node.x ?? 0, node.y ?? 0, durationMs);
|
|
415
|
+
graph.zoom(zoom, durationMs);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function renderEmptyDetails() {
|
|
419
|
+
detailsElement.innerHTML = '<p>Select a node to inspect details and connections.</p>';
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function renderDetails(node) {
|
|
423
|
+
const neighbors = Array.from(state.neighborsByNodeId.get(node.id) ?? [])
|
|
424
|
+
.map((neighborId) => state.allNodeById.get(neighborId))
|
|
425
|
+
.filter(Boolean)
|
|
426
|
+
.sort((a, b) => a.title.localeCompare(b.title));
|
|
427
|
+
|
|
428
|
+
const tags = Array.isArray(node.tags) && node.tags.length > 0 ? node.tags.join(', ') : 'none';
|
|
429
|
+
const category = node.category || 'default';
|
|
430
|
+
const degree = Number(node.degree ?? neighbors.length);
|
|
431
|
+
const pathValue = node.path ?? '(unresolved link target)';
|
|
432
|
+
const nodeType = node.missing ? 'missing' : 'resolved';
|
|
433
|
+
|
|
434
|
+
const connectionItems = neighbors.length
|
|
435
|
+
? neighbors
|
|
436
|
+
.map((neighbor) => {
|
|
437
|
+
const color = colorForCategory(neighbor.category);
|
|
438
|
+
return `<li><a href="#" class="connection-link" data-node-id="${escapeHtml(neighbor.id)}" style="color:${color}">${escapeHtml(neighbor.title)}</a></li>`;
|
|
439
|
+
})
|
|
440
|
+
.join('')
|
|
441
|
+
: '<li>No direct connections</li>';
|
|
442
|
+
|
|
443
|
+
detailsElement.innerHTML = `
|
|
444
|
+
<div class="meta-label">Title</div>
|
|
445
|
+
<p class="meta-value">${escapeHtml(node.title)}</p>
|
|
446
|
+
<div class="meta-label">ID</div>
|
|
447
|
+
<p class="meta-value">${escapeHtml(node.id)}</p>
|
|
448
|
+
<div class="meta-label">Category</div>
|
|
449
|
+
<p class="meta-value">${escapeHtml(category)}</p>
|
|
450
|
+
<div class="meta-label">Type</div>
|
|
451
|
+
<p class="meta-value">${nodeType}</p>
|
|
452
|
+
<div class="meta-label">Tags</div>
|
|
453
|
+
<p class="meta-value">${escapeHtml(tags)}</p>
|
|
454
|
+
<div class="meta-label">Degree</div>
|
|
455
|
+
<p class="meta-value">${degree}</p>
|
|
456
|
+
<div class="meta-label">Path</div>
|
|
457
|
+
<p class="meta-value">${escapeHtml(pathValue)}</p>
|
|
458
|
+
<div class="meta-label">Connections (${neighbors.length})</div>
|
|
459
|
+
<ul class="connection-list">${connectionItems}</ul>
|
|
460
|
+
`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function updateStats() {
|
|
464
|
+
const totalNodes = state.stats?.nodeCount ?? state.allNodeById.size;
|
|
465
|
+
const totalLinks = state.stats?.edgeCount ?? state.allEdgeByKey.size;
|
|
466
|
+
const totalFiles = state.stats?.fileCount ?? totalNodes;
|
|
467
|
+
const visibleNodes = state.visibleNodeIds.size;
|
|
468
|
+
const label = `${visibleNodes}/${totalNodes} nodes • ${totalLinks} links • ${totalFiles} files`;
|
|
469
|
+
statsElement.textContent = state.wsVersion > 0 ? `${label} • live v${state.wsVersion}` : label;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function populateFilters() {
|
|
473
|
+
const currentCategory = state.category;
|
|
474
|
+
const currentTag = state.tag;
|
|
475
|
+
|
|
476
|
+
const categories = new Set(['all']);
|
|
477
|
+
const tags = new Set(['all']);
|
|
478
|
+
for (const node of state.allNodeById.values()) {
|
|
479
|
+
categories.add(node.category || 'default');
|
|
480
|
+
for (const tag of Array.isArray(node.tags) ? node.tags : []) {
|
|
481
|
+
if (tag) {
|
|
482
|
+
tags.add(tag);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
setSelectOptions(categoryFilterElement, Array.from(categories).sort(sortFilterOption), currentCategory, 'All categories');
|
|
488
|
+
setSelectOptions(tagFilterElement, Array.from(tags).sort(sortFilterOption), currentTag, 'All tags');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function sortFilterOption(a, b) {
|
|
492
|
+
if (a === 'all') return -1;
|
|
493
|
+
if (b === 'all') return 1;
|
|
494
|
+
return a.localeCompare(b);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function setSelectOptions(selectElement, values, currentValue, allLabel) {
|
|
498
|
+
selectElement.innerHTML = values
|
|
499
|
+
.map((value) => `<option value="${escapeHtml(value)}">${escapeHtml(value === 'all' ? allLabel : value)}</option>`)
|
|
500
|
+
.join('');
|
|
501
|
+
|
|
502
|
+
if (values.includes(currentValue)) {
|
|
503
|
+
selectElement.value = currentValue;
|
|
504
|
+
} else {
|
|
505
|
+
selectElement.value = 'all';
|
|
506
|
+
if (selectElement === categoryFilterElement) {
|
|
507
|
+
state.category = 'all';
|
|
508
|
+
} else if (selectElement === tagFilterElement) {
|
|
509
|
+
state.tag = 'all';
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function isNodeVisible(node) {
|
|
515
|
+
const matchesCategory = state.category === 'all' || (node.category || 'default') === state.category;
|
|
516
|
+
if (!matchesCategory) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const matchesTag = state.tag === 'all' || (Array.isArray(node.tags) && node.tags.includes(state.tag));
|
|
521
|
+
if (!matchesTag) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const isMissing = Boolean(node.missing);
|
|
526
|
+
if (state.nodeType === 'missing' && !isMissing) {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
if (state.nodeType === 'resolved' && isMissing) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (!state.searchTerm) {
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const haystack = [node.id, node.title, node.category, Array.isArray(node.tags) ? node.tags.join(' ') : '']
|
|
538
|
+
.join(' ')
|
|
539
|
+
.toLowerCase();
|
|
540
|
+
return haystack.includes(state.searchTerm);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function renderNode(node, ctx, globalScale) {
|
|
544
|
+
void globalScale;
|
|
545
|
+
const radius = getNodeRadius(node);
|
|
546
|
+
const nodeId = node.id;
|
|
547
|
+
const isFocused = state.selectedNodeId === nodeId || state.hoveredNodeId === nodeId;
|
|
548
|
+
const isHighlighted = state.highlightedNodeIds.has(nodeId);
|
|
549
|
+
const hasFocusContext = Boolean(state.selectedNodeId || state.hoveredNodeId);
|
|
550
|
+
const baseColor = getNodeColor(node);
|
|
551
|
+
const finalColor = hasFocusContext && !isHighlighted ? DIMMED_NODE_COLOR : baseColor;
|
|
552
|
+
|
|
553
|
+
ctx.beginPath();
|
|
554
|
+
ctx.arc(node.x, node.y, radius + (isFocused ? 1.2 : 0), 0, Math.PI * 2, false);
|
|
555
|
+
ctx.fillStyle = finalColor;
|
|
556
|
+
ctx.fill();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function getNodeRadius(node) {
|
|
560
|
+
const degree = Number(node.degree ?? 0);
|
|
561
|
+
return 2.7 + Math.min(7.6, Math.sqrt(degree + 1) * 1.08);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function getNodeColor(node) {
|
|
565
|
+
if (node.missing) {
|
|
566
|
+
return '#ffc58b';
|
|
567
|
+
}
|
|
568
|
+
if (state.highlightedNodeIds.has(node.id)) {
|
|
569
|
+
return '#f3faff';
|
|
570
|
+
}
|
|
571
|
+
return colorForCategory(node.category);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function getLinkColor(link) {
|
|
575
|
+
const key = linkKey(link);
|
|
576
|
+
if (state.highlightedEdgeKeys.has(key)) {
|
|
577
|
+
return HIGHLIGHT_LINK_COLOR;
|
|
578
|
+
}
|
|
579
|
+
if (state.selectedNodeId || state.hoveredNodeId) {
|
|
580
|
+
return DIMMED_LINK_COLOR;
|
|
581
|
+
}
|
|
582
|
+
return NORMAL_LINK_COLOR;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function getLinkWidth(link) {
|
|
586
|
+
const key = linkKey(link);
|
|
587
|
+
if (state.highlightedEdgeKeys.has(key)) {
|
|
588
|
+
return 2.1;
|
|
589
|
+
}
|
|
590
|
+
return 1;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function linkKey(link) {
|
|
594
|
+
if (link?._key) {
|
|
595
|
+
return link._key;
|
|
596
|
+
}
|
|
597
|
+
const sourceId = typeof link.source === 'object' ? link.source.id : String(link.source);
|
|
598
|
+
const targetId = typeof link.target === 'object' ? link.target.id : String(link.target);
|
|
599
|
+
const edgeType = String(link.type ?? '');
|
|
600
|
+
const edgeLabel = String(link.label ?? '');
|
|
601
|
+
return toEdgeKey(sourceId, targetId, edgeType, edgeLabel);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function colorForCategory(category) {
|
|
605
|
+
return CATEGORY_COLORS[category] ?? CATEGORY_COLORS.default;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function startRealtime() {
|
|
609
|
+
let reconnectDelayMs = 800;
|
|
610
|
+
let socket = null;
|
|
611
|
+
let reconnectTimer = null;
|
|
612
|
+
|
|
613
|
+
const scheduleReconnect = () => {
|
|
614
|
+
if (reconnectTimer) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
reconnectTimer = setTimeout(() => {
|
|
618
|
+
reconnectTimer = null;
|
|
619
|
+
connect();
|
|
620
|
+
}, reconnectDelayMs);
|
|
621
|
+
reconnectDelayMs = Math.min(10_000, Math.round(reconnectDelayMs * 1.8));
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const connect = () => {
|
|
625
|
+
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
|
629
|
+
const activeSocket = new WebSocket(`${protocol}://${window.location.host}/ws`);
|
|
630
|
+
socket = activeSocket;
|
|
631
|
+
setRealtimeStatus({ text: 'Realtime: connecting...', level: 'warn' });
|
|
632
|
+
|
|
633
|
+
activeSocket.addEventListener('open', () => {
|
|
634
|
+
if (socket !== activeSocket) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
reconnectDelayMs = 800;
|
|
638
|
+
state.wsConnected = true;
|
|
639
|
+
setRealtimeStatus({ text: 'Realtime: connected', level: 'ok' });
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
activeSocket.addEventListener('message', (event) => {
|
|
643
|
+
if (socket !== activeSocket) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const message = parseMessage(event.data);
|
|
647
|
+
if (!message) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (message.type === 'graph:init') {
|
|
651
|
+
const version = Number(message.payload?.version ?? 0);
|
|
652
|
+
if (version >= state.wsVersion) {
|
|
653
|
+
state.wsVersion = version;
|
|
654
|
+
applySnapshot(message.payload?.graph ?? {}, { shouldLazyLoad: true, shouldFit: false });
|
|
655
|
+
updateStats();
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (message.type === 'graph:patch') {
|
|
660
|
+
const version = Number(message.payload?.version ?? 0);
|
|
661
|
+
if (version <= state.wsVersion) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
state.wsVersion = version;
|
|
665
|
+
applyPatch(message.payload);
|
|
666
|
+
setRealtimeStatus({ text: `Realtime: updated (${message.payload?.reason ?? 'patch'})`, level: 'ok' });
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
activeSocket.addEventListener('error', () => {});
|
|
671
|
+
|
|
672
|
+
activeSocket.addEventListener('close', () => {
|
|
673
|
+
if (socket !== activeSocket) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
state.wsConnected = false;
|
|
677
|
+
setRealtimeStatus({ text: 'Realtime: reconnecting...', level: 'warn' });
|
|
678
|
+
socket = null;
|
|
679
|
+
scheduleReconnect();
|
|
680
|
+
});
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
connect();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function enableWebglRendererIfSupported() {
|
|
687
|
+
const renderers = window.ForceGraph?.renderers;
|
|
688
|
+
const webglRenderer = renderers?.webgl;
|
|
689
|
+
if (typeof graph.graphRenderer === 'function' && webglRenderer) {
|
|
690
|
+
graph.graphRenderer(webglRenderer);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function parseMessage(value) {
|
|
695
|
+
if (typeof value !== 'string') {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
return JSON.parse(value);
|
|
700
|
+
} catch (_error) {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function setRealtimeStatus({ text, level }) {
|
|
706
|
+
realtimeStatusElement.textContent = text;
|
|
707
|
+
realtimeStatusElement.classList.remove('ok', 'warn');
|
|
708
|
+
if (level) {
|
|
709
|
+
realtimeStatusElement.classList.add(level);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function setTvMode(enabled, { updateQuery, useFullscreen }) {
|
|
714
|
+
state.tvMode = enabled;
|
|
715
|
+
document.body.classList.toggle('tv-mode', enabled);
|
|
716
|
+
tvModeButtonElement.setAttribute('aria-pressed', enabled ? 'true' : 'false');
|
|
717
|
+
tvModeButtonElement.textContent = enabled ? 'Exit TV Mode' : 'TV Mode';
|
|
718
|
+
resizeGraphToContainer();
|
|
719
|
+
graph.refresh();
|
|
720
|
+
|
|
721
|
+
if (updateQuery) {
|
|
722
|
+
const url = new URL(window.location.href);
|
|
723
|
+
if (enabled) {
|
|
724
|
+
url.searchParams.set('tv', '1');
|
|
725
|
+
} else {
|
|
726
|
+
url.searchParams.delete('tv');
|
|
727
|
+
}
|
|
728
|
+
window.history.replaceState({}, '', url);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (useFullscreen && enabled && document.fullscreenElement == null) {
|
|
732
|
+
void document.documentElement.requestFullscreen().catch(() => {});
|
|
733
|
+
}
|
|
734
|
+
if (useFullscreen && !enabled && document.fullscreenElement != null) {
|
|
735
|
+
void document.exitFullscreen().catch(() => {});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function markInteraction() {
|
|
740
|
+
state.lastInteractionAt = performance.now();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function queueGraphDataUpdate(nodes, links) {
|
|
744
|
+
state.pendingGraphData = { nodes, links };
|
|
745
|
+
const now = performance.now();
|
|
746
|
+
const elapsedMs = now - state.lastGraphDataApplyAt;
|
|
747
|
+
if (elapsedMs >= GRAPH_UPDATE_THROTTLE_MS && state.graphDataThrottleTimer == null) {
|
|
748
|
+
applyPendingGraphData();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (state.graphDataThrottleTimer != null) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const delayMs = Math.max(0, GRAPH_UPDATE_THROTTLE_MS - elapsedMs);
|
|
755
|
+
state.graphDataThrottleTimer = setTimeout(() => {
|
|
756
|
+
state.graphDataThrottleTimer = null;
|
|
757
|
+
applyPendingGraphData();
|
|
758
|
+
}, delayMs);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function applyPendingGraphData() {
|
|
762
|
+
if (!state.pendingGraphData) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const { nodes, links } = state.pendingGraphData;
|
|
766
|
+
state.pendingGraphData = null;
|
|
767
|
+
state.lastGraphDataApplyAt = performance.now();
|
|
768
|
+
graph.graphData({ nodes, links });
|
|
769
|
+
graph.d3ReheatSimulation();
|
|
770
|
+
syncHighlights();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function areSetsEqual(left, right) {
|
|
774
|
+
if (left.size !== right.size) {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
for (const value of left) {
|
|
778
|
+
if (!right.has(value)) {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function toEdgeKey(sourceId, targetId, edgeType = '', edgeLabel = '') {
|
|
786
|
+
return `${sourceId}=>${targetId}:${edgeType}:${edgeLabel}`;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function escapeHtml(value) {
|
|
790
|
+
return String(value)
|
|
791
|
+
.replaceAll('&', '&')
|
|
792
|
+
.replaceAll('<', '<')
|
|
793
|
+
.replaceAll('>', '>')
|
|
794
|
+
.replaceAll('"', '"')
|
|
795
|
+
.replaceAll("'", ''');
|
|
796
|
+
}
|