@hongmaple0820/scale-engine 0.50.1 → 0.50.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/api/http.js +3 -1
  4. package/dist/api/http.js.map +1 -1
  5. package/dist/cli/cortexCommands.d.ts +16 -0
  6. package/dist/cli/cortexCommands.js +47 -4
  7. package/dist/cli/cortexCommands.js.map +1 -1
  8. package/dist/cortex/InstinctStore.d.ts +13 -1
  9. package/dist/cortex/InstinctStore.js +90 -11
  10. package/dist/cortex/InstinctStore.js.map +1 -1
  11. package/dist/cortex/SessionInjector.js +39 -2
  12. package/dist/cortex/SessionInjector.js.map +1 -1
  13. package/dist/dashboard/DashboardServer.d.ts +158 -0
  14. package/dist/dashboard/DashboardServer.js +753 -13
  15. package/dist/dashboard/DashboardServer.js.map +1 -1
  16. package/dist/dashboard/spa/assets/index-VYBCLBje.js +11 -0
  17. package/dist/dashboard/spa/assets/index-VhwY_ac1.css +1 -0
  18. package/dist/dashboard/spa/assets/naive-ui-BQy2AJkt.js +3340 -0
  19. package/dist/dashboard/spa/assets/vendor-BPU6aOYA.js +3 -0
  20. package/dist/dashboard/spa/assets/vue-CQQMb5Wi.js +17 -0
  21. package/dist/dashboard/spa/index.html +15 -462
  22. package/dist/memory/MemoryFabric.d.ts +13 -1
  23. package/dist/memory/MemoryFabric.js +60 -0
  24. package/dist/memory/MemoryFabric.js.map +1 -1
  25. package/dist/version.d.ts +1 -1
  26. package/dist/version.js +1 -1
  27. package/docs/workflow/ASSESSMENT_INDEX.md +326 -0
  28. package/docs/workflow/COMPARATIVE_ANALYSIS.md +422 -0
  29. package/docs/workflow/EXECUTIVE_SUMMARY.md +310 -0
  30. package/docs/workflow/IMPROVEMENT_CHECKLIST.md +518 -0
  31. package/docs/workflow/IMPROVEMENT_ROADMAP.md +707 -0
  32. package/docs/workflow/README.md +8 -0
  33. package/package.json +6 -2
  34. package/dist/dashboard/spa/app.js +0 -515
  35. package/dist/dashboard/spa/components/DataTable.js +0 -53
  36. package/dist/dashboard/spa/components/EventStream.js +0 -66
  37. package/dist/dashboard/spa/components/LoadingState.js +0 -39
  38. package/dist/dashboard/spa/components/MetricCard.js +0 -30
  39. package/dist/dashboard/spa/components/Panel.js +0 -27
  40. package/dist/dashboard/spa/components/StatusBadge.js +0 -51
  41. package/dist/dashboard/spa/i18n.js +0 -767
  42. package/dist/dashboard/spa/pages/costs.js +0 -522
  43. package/dist/dashboard/spa/pages/documents.js +0 -540
  44. package/dist/dashboard/spa/pages/knowledge.js +0 -457
  45. package/dist/dashboard/spa/pages/monitoring.js +0 -361
  46. package/dist/dashboard/spa/pages/overview.js +0 -301
  47. package/dist/dashboard/spa/pages/topology-renderers.js +0 -251
  48. package/dist/dashboard/spa/pages/topology.js +0 -370
  49. package/dist/dashboard/spa/pages/workflow-renderers.js +0 -239
  50. package/dist/dashboard/spa/pages/workflow.js +0 -217
@@ -1,251 +0,0 @@
1
- /**
2
- * Safe DOM renderers for the topology page.
3
- */
4
- ;(() => {
5
- 'use strict'
6
-
7
- const { t, dom } = window.Dashboard
8
- const { el, emptyState } = dom
9
-
10
- const LAYER_COLORS = {
11
- api: '#00dc82',
12
- service: '#5588ff',
13
- data: '#ffaa00',
14
- ui: '#ff6688',
15
- utility: '#aa88ff',
16
- config: '#888888',
17
- test: '#44cccc',
18
- unknown: '#555555',
19
- }
20
-
21
- function renderLayout(app) {
22
- app.replaceChildren(
23
- el('div', { className: 'topology-controls', id: 'topo-controls' }, [
24
- layoutButton('cose', t('topology.force'), true),
25
- layoutButton('breadthfirst', t('topology.tree')),
26
- layoutButton('circle', t('topology.circle')),
27
- layoutButton('concentric', t('topology.concentric')),
28
- layoutButton('dagre', t('topology.dag')),
29
- separator(),
30
- el('button', { className: 'topo-btn', id: 'topo-fit', text: t('topology.fitView'), title: t('topology.fitView') }),
31
- el('button', { className: 'topo-btn', id: 'topo-export-png', text: `PNG ${t('topology.exportPNG')}`, title: t('topology.exportPNG') }),
32
- el('button', { className: 'topo-btn', id: 'topo-export-json', text: `JSON ${t('topology.exportJSON')}`, title: t('topology.exportJSON') }),
33
- separator(),
34
- el('input', { id: 'topo-filter', type: 'text', className: 'search-box', placeholder: t('topology.searchNodes'), style: { width: '180px' } }),
35
- el('span', { id: 'topo-stats', className: 'text-muted text-sm', style: { alignSelf: 'center' } }),
36
- ]),
37
- el('div', { style: { display: 'flex', gap: '16px', height: 'calc(100vh - 140px)' } }, [
38
- el('div', { style: { flex: '1', minWidth: '0', position: 'relative' } }, [
39
- el('div', { id: 'topology-cy', style: { width: '100%', height: '100%' } }),
40
- el('div', { id: 'topo-minimap', style: minimapStyle() }),
41
- ]),
42
- el('div', { id: 'topo-sidebar', style: sidebarStyle() }, [
43
- panel('topo-detail', [el('div', { className: 'panel-title', text: t('topology.nodes') }), el('div', { className: 'text-muted text-sm', text: t('common.search') })]),
44
- panel('topo-legend', [el('div', { className: 'panel-title' }, [document.createTextNode(t('topology.layers')), el('span', { className: 'count', id: 'topo-layer-count' })]), el('div', { id: 'topo-layer-legend' })]),
45
- panel('topo-domains-panel', [el('div', { className: 'panel-title', text: t('topology.domains') }), el('div', { id: 'topo-domains' })]),
46
- panel('topo-kind-filter', [el('div', { className: 'panel-title', text: t('topology.kinds') }), el('div', { id: 'topo-kind-legend' })]),
47
- ]),
48
- ])
49
- )
50
- }
51
-
52
- function renderNoData(container) {
53
- if (container) container.replaceChildren(emptyState(t('topology.noData')))
54
- }
55
-
56
- function renderLayerLegend({ container, countNode, topologyData, activeLayerFilters, onToggle }) {
57
- if (!container || !topologyData) return
58
- const layers = countBy(topologyData.nodes, node => node.layer ?? 'unknown')
59
- const total = topologyData.nodes.length
60
- if (countNode) countNode.textContent = t('topology.layerNodeStats', { layers: Object.keys(layers).length, nodes: total })
61
- container.replaceChildren(...Object.entries(layers)
62
- .sort((left, right) => right[1] - left[1])
63
- .map(([layer, count]) => legendItem({
64
- className: 'topo-legend-item',
65
- dataset: { layer },
66
- active: !activeLayerFilters.has(layer),
67
- swatch: swatch(LAYER_COLORS[layer] || '#555', false),
68
- label: layer,
69
- meta: `${count} (${((count / total) * 100).toFixed(1)}%)`,
70
- onClick: () => onToggle(layer),
71
- })))
72
- }
73
-
74
- function renderKindFilter({ container, topologyData, activeKindFilters, onToggle }) {
75
- if (!container || !topologyData) return
76
- const kinds = countBy(topologyData.nodes, node => node.kind ?? 'unknown')
77
- container.replaceChildren(...Object.entries(kinds)
78
- .sort((left, right) => right[1] - left[1])
79
- .map(([kind, count]) => legendItem({
80
- className: 'topo-kind-item',
81
- dataset: { kind },
82
- active: !activeKindFilters.has(kind),
83
- label: kind,
84
- meta: String(count),
85
- onClick: () => onToggle(kind),
86
- })))
87
- }
88
-
89
- function renderDomainPanel({ container, domainData, onSelect }) {
90
- if (!container) return
91
- if (!domainData?.domains?.length) {
92
- container.replaceChildren(el('div', { className: 'text-muted text-sm', text: t('topology.noDomains') }))
93
- return
94
- }
95
- const children = domainData.domains.slice(0, 12).map(domain => legendItem({
96
- className: 'topo-domain-item',
97
- dataset: { domain: domain.name },
98
- swatch: swatch(hashColor(domain.name), true),
99
- label: domain.name,
100
- meta: String(domain.nodes?.length ?? 0),
101
- onClick: () => onSelect(domain),
102
- }))
103
- if (domainData.flows?.length) children.push(flowList(domainData.flows.slice(0, 5)))
104
- container.replaceChildren(...children)
105
- }
106
-
107
- function renderDomainDetail(panelNode, domain) {
108
- if (!panelNode || !domain) return
109
- const color = hashColor(domain.name)
110
- const rows = (domain.nodes ?? []).slice(0, 20).map(node => {
111
- const row = el('div', { style: { fontSize: '12px', padding: '3px 0', color: 'var(--text-1)' } })
112
- row.append(document.createTextNode(node.name ?? ''))
113
- row.append(el('span', { text: ` (${node.kind ?? '-'})`, style: { color: 'var(--text-2)' } }))
114
- return row
115
- })
116
- if ((domain.nodes?.length ?? 0) > 20) {
117
- rows.push(el('div', { text: t('topology.moreNodes', { count: domain.nodes.length - 20 }), style: { fontSize: '12px', color: 'var(--text-2)' } }))
118
- }
119
- panelNode.replaceChildren(
120
- el('div', { className: 'panel-title', text: domain.name, style: { color } }),
121
- el('div', { text: `${domain.nodes?.length ?? 0} nodes`, style: { fontSize: '13px', color: 'var(--text-1)', marginBottom: '8px' } }),
122
- el('div', { style: { maxHeight: '200px', overflowY: 'auto' } }, rows)
123
- )
124
- }
125
-
126
- function renderMinimapCanvas(container) {
127
- const canvas = el('canvas', { attrs: { width: 160, height: 120 } })
128
- container.replaceChildren(canvas)
129
- return canvas
130
- }
131
-
132
- function renderNodeEmpty(panelNode) {
133
- if (!panelNode) return
134
- panelNode.replaceChildren(
135
- el('div', { className: 'panel-title', text: t('topology.nodeDetails') }),
136
- el('div', { className: 'text-muted text-sm', text: t('topology.clickNodeHint') })
137
- )
138
- }
139
-
140
- function renderNodeDetail(panelNode, data, metrics) {
141
- if (!panelNode) return
142
- const layerColor = LAYER_COLORS[data.layer] || '#888'
143
- const children = [
144
- el('div', { className: 'panel-title', text: data.kind ?? '-', style: { color: layerColor } }),
145
- el('div', { text: data.label ?? '', style: { fontSize: '15px', fontWeight: '600', marginBottom: '12px' } }),
146
- nodeMeta(data, metrics, layerColor),
147
- ]
148
- if (metrics.callers.length > 0) children.push(nameList(t('topology.calledBy', { count: metrics.callers.length }), metrics.callers))
149
- if (metrics.callees.length > 0) children.push(nameList(t('topology.calls', { count: metrics.callees.length }), metrics.callees))
150
- panelNode.replaceChildren(...children)
151
- }
152
-
153
- function nodeMeta(data, metrics, layerColor) {
154
- const rows = [
155
- labelValue(t('topology.layer'), el('span', { text: data.layer ?? '-', style: { color: layerColor } })),
156
- labelValue(t('topology.file'), el('span', { text: data.filePath ?? '-', style: { wordBreak: 'break-all' } })),
157
- ]
158
- if (data.line) rows.push(labelValue(t('topology.line'), document.createTextNode(String(data.line))))
159
- if (data.signature) rows.push(labelValue(t('topology.signature'), el('code', { text: data.signature, style: { fontSize: '12px', wordBreak: 'break-all' } })))
160
- if (data.domain) rows.push(labelValue(t('topology.domain'), document.createTextNode(String(data.domain))))
161
- rows.push(labelValue(`${t('topology.degree')}:`, document.createTextNode(`in=${metrics.inDegree} out=${metrics.outDegree}`)))
162
- return el('div', { style: { fontSize: '13px', color: 'var(--text-1)' } }, rows)
163
- }
164
-
165
- function labelValue(label, valueNode) {
166
- const row = el('div', { style: { marginBottom: '6px' } })
167
- row.append(el('strong', { text: label }), document.createTextNode(' '), valueNode)
168
- return row
169
- }
170
-
171
- function nameList(title, names) {
172
- return el('div', { style: { marginTop: '10px', fontSize: '12px' } }, [
173
- el('div', { text: title, style: { color: 'var(--text-2)', marginBottom: '4px' } }),
174
- ...names.map(name => el('div', { text: name, style: { color: 'var(--text-1)', padding: '1px 0' } })),
175
- ])
176
- }
177
-
178
- function legendItem({ className, dataset = {}, active = true, swatch: swatchNode, label, meta, onClick }) {
179
- const node = el('div', { className, dataset, style: legendItemStyle(active) }, [
180
- swatchNode,
181
- el('span', { text: label, style: { color: 'var(--text-1)', flex: '1', overflow: 'hidden', textOverflow: 'ellipsis' } }),
182
- el('span', { text: meta, style: { color: 'var(--text-2)', fontSize: '11px', flexShrink: '0' } }),
183
- ].filter(Boolean))
184
- node.addEventListener('click', onClick)
185
- return node
186
- }
187
-
188
- function flowList(flows) {
189
- return el('div', { style: { marginTop: '12px', paddingTop: '8px', borderTop: '1px solid var(--border)' } }, [
190
- el('div', { text: t('topology.detectedFlows'), style: { fontSize: '11px', color: 'var(--text-2)', marginBottom: '6px' } }),
191
- ...flows.map(flow => el('div', { text: `${flow.from} -> ${flow.to}`, style: { fontSize: '12px', color: 'var(--text-1)', padding: '3px 0' } })),
192
- ])
193
- }
194
-
195
- function layoutButton(layout, label, active = false) {
196
- return el('button', { className: `topo-btn${active ? ' active' : ''}`, text: label, dataset: { layout } })
197
- }
198
-
199
- function panel(id, children) {
200
- return el('div', { className: 'panel', id }, children)
201
- }
202
-
203
- function separator() {
204
- return el('span', { style: { marginLeft: '12px', borderLeft: '1px solid var(--border)', paddingLeft: '12px' } })
205
- }
206
-
207
- function swatch(color, round) {
208
- return el('div', { style: { width: round ? '10px' : '12px', height: round ? '10px' : '12px', borderRadius: round ? '50%' : '3px', background: color, flexShrink: '0' } })
209
- }
210
-
211
- function legendItemStyle(active) {
212
- return { display: 'flex', alignItems: 'center', gap: '8px', padding: '5px 6px', borderRadius: '4px', cursor: 'pointer', fontSize: '13px', opacity: active ? '1' : '0.35' }
213
- }
214
-
215
- function sidebarStyle() {
216
- return { width: '300px', flexShrink: '0', display: 'flex', flexDirection: 'column', gap: '16px', overflowY: 'auto' }
217
- }
218
-
219
- function minimapStyle() {
220
- return { position: 'absolute', bottom: '12px', right: '12px', width: '160px', height: '120px', background: 'var(--bg-2)', border: '1px solid var(--border)', borderRadius: '6px', overflow: 'hidden', opacity: '0.8' }
221
- }
222
-
223
- function countBy(items, selector) {
224
- const counts = {}
225
- for (const item of items ?? []) {
226
- const key = selector(item)
227
- counts[key] = (counts[key] ?? 0) + 1
228
- }
229
- return counts
230
- }
231
-
232
- function hashColor(str) {
233
- const text = String(str ?? '')
234
- let hash = 0
235
- for (let i = 0; i < text.length; i++) hash = text.charCodeAt(i) + ((hash << 5) - hash)
236
- return `hsl(${hash % 360}, 60%, 55%)`
237
- }
238
-
239
- window.DashboardTopologyRenderers = {
240
- LAYER_COLORS,
241
- renderDomainDetail,
242
- renderDomainPanel,
243
- renderKindFilter,
244
- renderLayerLegend,
245
- renderLayout,
246
- renderMinimapCanvas,
247
- renderNoData,
248
- renderNodeDetail,
249
- renderNodeEmpty,
250
- }
251
- })()
@@ -1,370 +0,0 @@
1
- /**
2
- * Topology page controller. Rendering lives in topology-renderers.js so this
3
- * file stays focused on data flow, Cytoscape setup, and interactions.
4
- */
5
- ;(() => {
6
- 'use strict'
7
-
8
- const { fetchJSON, registerChart, getTheme, t, $, $$ } = window.Dashboard
9
- const renderers = window.DashboardTopologyRenderers
10
- const { LAYER_COLORS } = renderers
11
-
12
- let cy = null
13
- let topologyData = null
14
- let domainData = null
15
- let activeLayerFilters = new Set()
16
- let activeKindFilters = new Set()
17
-
18
- async function renderTopology() {
19
- const app = $('#app')
20
- renderers.renderLayout(app)
21
-
22
- const [topo, domains] = await Promise.all([
23
- fetchJSON('/api/topology'),
24
- fetchJSON('/api/topology/domains'),
25
- ])
26
-
27
- topologyData = topo
28
- domainData = domains
29
- cy = null
30
-
31
- if (!topologyData?.nodes?.length) {
32
- renderers.renderNoData($('#topology-cy'))
33
- return
34
- }
35
-
36
- renderLayerLegend()
37
- renderDomainPanel()
38
- renderKindFilter()
39
- initCytoscape(topologyData)
40
- wireControls()
41
- wireKeyboard()
42
- }
43
-
44
- function renderLayerLegend() {
45
- renderers.renderLayerLegend({
46
- container: $('#topo-layer-legend'),
47
- countNode: $('#topo-layer-count'),
48
- topologyData,
49
- activeLayerFilters,
50
- onToggle: layer => {
51
- toggleFilter(activeLayerFilters, layer)
52
- renderLayerLegend()
53
- applyFilters()
54
- },
55
- })
56
- }
57
-
58
- function renderKindFilter() {
59
- renderers.renderKindFilter({
60
- container: $('#topo-kind-legend'),
61
- topologyData,
62
- activeKindFilters,
63
- onToggle: kind => {
64
- toggleFilter(activeKindFilters, kind)
65
- renderKindFilter()
66
- applyFilters()
67
- },
68
- })
69
- }
70
-
71
- function renderDomainPanel() {
72
- renderers.renderDomainPanel({
73
- container: $('#topo-domains'),
74
- domainData,
75
- onSelect: domain => {
76
- if (!domain || !cy) return
77
- const nodeIds = new Set((domain.nodes ?? []).map(node => node.id))
78
- cy.elements().removeClass('highlighted dimmed')
79
- cy.nodes().forEach(node => {
80
- node.addClass(nodeIds.has(node.id()) ? 'highlighted' : 'dimmed')
81
- })
82
- showDomainDetail(domain)
83
- },
84
- })
85
- }
86
-
87
- function showDomainDetail(domain) {
88
- renderers.renderDomainDetail($('#topo-detail'), domain)
89
- }
90
-
91
- function initCytoscape(data) {
92
- if (!window.cytoscape) {
93
- const script = document.createElement('script')
94
- script.src = 'https://cdn.jsdelivr.net/npm/cytoscape@3/dist/cytoscape.min.js'
95
- script.onload = () => {
96
- const dagre = document.createElement('script')
97
- dagre.src = 'https://cdn.jsdelivr.net/npm/cytoscape-dagre@2/cytoscape-dagre.min.js'
98
- dagre.onload = () => buildGraph(data)
99
- dagre.onerror = () => buildGraph(data)
100
- document.head.appendChild(dagre)
101
- }
102
- document.head.appendChild(script)
103
- return
104
- }
105
- buildGraph(data)
106
- }
107
-
108
- function buildGraph(data) {
109
- const container = $('#topology-cy')
110
- if (!container) return
111
-
112
- const maxNodes = 800
113
- const degreeMap = buildDegreeMap(data.edges)
114
- const sortedNodes = [...data.nodes].sort((a, b) => (degreeMap.get(b.id) ?? 0) - (degreeMap.get(a.id) ?? 0))
115
- const nodes = sortedNodes.slice(0, maxNodes)
116
- const nodeIds = new Set(nodes.map(node => node.id))
117
- const edges = data.edges.filter(edge => nodeIds.has(edge.source) && nodeIds.has(edge.target))
118
- const maxDegree = Math.max(...nodes.map(node => degreeMap.get(node.id) ?? 0), 1)
119
-
120
- $('#topo-stats').textContent = `${nodes.length}/${data.nodes.length} ${t('topology.nodes')}, ${edges.length} ${t('topology.edges')}`
121
- cy = cytoscape({
122
- container,
123
- elements: [
124
- ...nodes.map(node => graphNode(node, degreeMap, maxDegree)),
125
- ...edges.map((edge, index) => ({ data: { id: `e${index}`, source: edge.source, target: edge.target, kind: edge.kind } })),
126
- ],
127
- style: graphStyles(),
128
- layout: { name: 'cose', animate: false, padding: 30, nodeRepulsion: () => 4000 },
129
- minZoom: 0.05,
130
- maxZoom: 10,
131
- wheelSensitivity: 0.3,
132
- })
133
-
134
- cy.on('mouseover', 'node', event => {
135
- const node = event.target
136
- const neighborhood = node.neighborhood().add(node)
137
- cy.elements().removeClass('highlighted').not(neighborhood).addClass('dimmed')
138
- neighborhood.removeClass('dimmed').addClass('highlighted')
139
- })
140
- cy.on('mouseout', 'node', () => cy.elements().removeClass('highlighted dimmed'))
141
- cy.on('tap', 'node', event => {
142
- showNodeDetail(event.target.data())
143
- highlightNeighbors(event.target)
144
- })
145
- cy.on('tap', event => {
146
- if (event.target === cy) {
147
- cy.elements().removeClass('highlighted dimmed')
148
- showNodeDetail(null)
149
- }
150
- })
151
-
152
- renderMinimap()
153
- }
154
-
155
- function buildDegreeMap(edges) {
156
- const degreeMap = new Map()
157
- for (const edge of edges) {
158
- degreeMap.set(edge.source, (degreeMap.get(edge.source) ?? 0) + 1)
159
- degreeMap.set(edge.target, (degreeMap.get(edge.target) ?? 0) + 1)
160
- }
161
- return degreeMap
162
- }
163
-
164
- function graphNode(node, degreeMap, maxDegree) {
165
- const degree = degreeMap.get(node.id) ?? 0
166
- return {
167
- data: {
168
- id: node.id,
169
- label: node.name,
170
- layer: node.layer ?? 'unknown',
171
- kind: node.kind,
172
- filePath: node.filePath,
173
- line: node.line,
174
- signature: node.signature,
175
- domain: node.domain,
176
- degree,
177
- size: 8 + Math.round((degree / maxDegree) * 20),
178
- },
179
- }
180
- }
181
-
182
- function graphStyles() {
183
- return [
184
- {
185
- selector: 'node',
186
- style: {
187
- 'background-color': ele => LAYER_COLORS[ele.data('layer')] || '#555',
188
- label: ele => ele.data('degree') > 3 ? ele.data('label') : '',
189
- color: '#a1a1a1',
190
- 'font-size': '10px',
191
- 'text-valign': 'bottom',
192
- 'text-margin-y': 5,
193
- width: 'data(size)',
194
- height: 'data(size)',
195
- 'border-width': 1,
196
- 'border-color': '#333',
197
- 'transition-property': 'background-color, border-color, opacity',
198
- 'transition-duration': '0.15s',
199
- },
200
- },
201
- { selector: 'node:selected', style: { 'border-width': 3, 'border-color': '#00dc82', 'font-weight': 'bold', label: 'data(label)' } },
202
- { selector: 'edge', style: { width: 1, 'line-color': '#333', 'target-arrow-color': '#333', 'target-arrow-shape': 'triangle', 'curve-style': 'bezier', opacity: 0.3, 'transition-property': 'line-color, opacity, width', 'transition-duration': '0.15s' } },
203
- { selector: '.highlighted', style: { 'background-color': '#00dc82', 'line-color': '#00dc82', 'target-arrow-color': '#00dc82', opacity: 1, width: 2, 'z-index': 10 } },
204
- { selector: '.search-match', style: { 'border-width': 3, 'border-color': '#ffaa00', 'background-color': '#ffaa00', label: 'data(label)', 'z-index': 20 } },
205
- { selector: '.dimmed', style: { opacity: 0.08 } },
206
- ]
207
- }
208
-
209
- function highlightNeighbors(node) {
210
- const neighborhood = node.neighborhood().add(node)
211
- cy.elements().removeClass('highlighted').addClass('dimmed')
212
- neighborhood.removeClass('dimmed').addClass('highlighted')
213
- }
214
-
215
- function renderMinimap() {
216
- const miniContainer = $('#topo-minimap')
217
- if (!miniContainer || !cy) return
218
- const canvas = renderers.renderMinimapCanvas(miniContainer)
219
-
220
- function drawMinimap() {
221
- const ctx = canvas.getContext('2d')
222
- ctx.clearRect(0, 0, 160, 120)
223
- ctx.fillStyle = getTheme() === 'dark' ? '#111' : '#f5f5f5'
224
- ctx.fillRect(0, 0, 160, 120)
225
-
226
- const bb = cy.elements().boundingBox()
227
- if (bb.w === 0 || bb.h === 0) return
228
- const scale = Math.min(150 / bb.w, 110 / bb.h)
229
- const offsetX = (160 - bb.w * scale) / 2 - bb.x1 * scale
230
- const offsetY = (120 - bb.h * scale) / 2 - bb.y1 * scale
231
-
232
- cy.nodes().forEach(node => {
233
- const pos = node.position()
234
- ctx.fillStyle = LAYER_COLORS[node.data('layer')] || '#555'
235
- ctx.fillRect(pos.x * scale + offsetX - 1, pos.y * scale + offsetY - 1, 2, 2)
236
- })
237
-
238
- const ext = cy.extent()
239
- ctx.strokeStyle = '#00dc82'
240
- ctx.lineWidth = 1
241
- ctx.strokeRect(ext.x1 * scale + offsetX, ext.y1 * scale + offsetY, ext.w * scale, ext.h * scale)
242
- }
243
-
244
- cy.on('viewport', drawMinimap)
245
- drawMinimap()
246
- }
247
-
248
- function showNodeDetail(data) {
249
- const panel = $('#topo-detail')
250
- if (!data) {
251
- renderers.renderNodeEmpty(panel)
252
- return
253
- }
254
- const node = cy?.getElementById(data.id)
255
- renderers.renderNodeDetail(panel, data, {
256
- inDegree: node?.indegree?.() ?? 0,
257
- outDegree: node?.outdegree?.() ?? 0,
258
- callers: node?.incomers('node').map(item => item.data('label')).slice(0, 10) ?? [],
259
- callees: node?.outgoers('node').map(item => item.data('label')).slice(0, 10) ?? [],
260
- })
261
- }
262
-
263
- function applyFilters() {
264
- if (!cy) return
265
- const searchQ = ($('#topo-filter')?.value ?? '').toLowerCase()
266
-
267
- cy.nodes().forEach(node => {
268
- const layer = node.data('layer')
269
- const kind = node.data('kind')
270
- const label = String(node.data('label') ?? '').toLowerCase()
271
- const filePath = String(node.data('filePath') ?? '').toLowerCase()
272
- const layerOk = activeLayerFilters.size === 0 || !activeLayerFilters.has(layer)
273
- const kindOk = activeKindFilters.size === 0 || !activeKindFilters.has(kind)
274
- const searchOk = !searchQ || label.includes(searchQ) || filePath.includes(searchQ)
275
-
276
- node.style('display', layerOk && kindOk ? 'element' : 'none')
277
- node.toggleClass('search-match', Boolean(searchQ && searchOk && layerOk && kindOk))
278
- })
279
-
280
- cy.edges().forEach(edge => {
281
- const visible = edge.source().style('display') !== 'none' && edge.target().style('display') !== 'none'
282
- edge.style('display', visible ? 'element' : 'none')
283
- })
284
- updateVisibleStats()
285
- }
286
-
287
- function updateVisibleStats() {
288
- if (!cy) return
289
- const visible = cy.nodes().filter(node => node.style('display') !== 'none').length
290
- const total = topologyData?.nodes?.length ?? 0
291
- const visibleEdges = cy.edges().filter(edge => edge.style('display') !== 'none').length
292
- $('#topo-stats').textContent = `${visible}/${total} ${t('topology.nodes')}, ${visibleEdges} ${t('topology.edges')}`
293
- }
294
-
295
- function wireControls() {
296
- $$('.topo-btn[data-layout]', $('#topo-controls')).forEach(btn => {
297
- btn.addEventListener('click', () => {
298
- $$('.topo-btn[data-layout]').forEach(item => item.classList.remove('active'))
299
- btn.classList.add('active')
300
- if (!cy) return
301
- const opts = { name: btn.dataset.layout, animate: true, padding: 30 }
302
- if (opts.name === 'dagre' && cy.dagre) {
303
- opts.rankDir = 'TB'
304
- opts.rankSep = 50
305
- }
306
- cy.layout(opts).run()
307
- setTimeout(renderMinimap, 600)
308
- })
309
- })
310
-
311
- $('#topo-fit')?.addEventListener('click', () => {
312
- if (cy) { cy.fit(undefined, 30); renderMinimap() }
313
- })
314
- $('#topo-export-png')?.addEventListener('click', exportPng)
315
- $('#topo-export-json')?.addEventListener('click', exportJson)
316
-
317
- let searchTimer = null
318
- $('#topo-filter')?.addEventListener('input', () => {
319
- clearTimeout(searchTimer)
320
- searchTimer = setTimeout(() => applyFilters(), 150)
321
- })
322
- }
323
-
324
- function exportPng() {
325
- if (!cy) return
326
- const link = document.createElement('a')
327
- link.href = cy.png({ bg: getTheme() === 'dark' ? '#0a0a0a' : '#ffffff', full: true, scale: 2 })
328
- link.download = 'topology.png'
329
- link.click()
330
- }
331
-
332
- function exportJson() {
333
- if (!topologyData) return
334
- const blob = new Blob([JSON.stringify(topologyData, null, 2)], { type: 'application/json' })
335
- const link = document.createElement('a')
336
- link.href = URL.createObjectURL(blob)
337
- link.download = 'topology.json'
338
- link.click()
339
- URL.revokeObjectURL(link.href)
340
- }
341
-
342
- function wireKeyboard() {
343
- document.addEventListener('keydown', event => {
344
- if ((event.ctrlKey || event.metaKey) && event.key === 'f') {
345
- event.preventDefault()
346
- $('#topo-filter')?.focus()
347
- }
348
- if (event.key === 'Escape') {
349
- const filter = $('#topo-filter')
350
- if (filter) filter.value = ''
351
- if (cy) {
352
- cy.elements().removeClass('highlighted dimmed search-match').style('display', 'element')
353
- showNodeDetail(null)
354
- }
355
- applyFilters()
356
- }
357
- if (event.key === 'f' && !event.ctrlKey && !event.metaKey && document.activeElement.tagName !== 'INPUT') {
358
- if (cy) cy.fit(undefined, 30)
359
- }
360
- })
361
- }
362
-
363
- function toggleFilter(filters, value) {
364
- if (filters.has(value)) filters.delete(value)
365
- else filters.add(value)
366
- }
367
-
368
- window.DashboardPages = window.DashboardPages || {}
369
- window.DashboardPages.topology = renderTopology
370
- })()