nexus-prime 6.2.0 → 6.3.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/dashboard/app/api.js +4 -3
- package/dist/dashboard/app/main.js +13 -1
- package/dist/dashboard/app/state.js +1 -0
- package/dist/dashboard/app/views/board.js +83 -7
- package/dist/dashboard/app/views/knowledge.js +30 -7
- package/dist/dashboard/app/views/license.js +7 -7
- package/dist/dashboard/app/views/memory.js +61 -3
- package/dist/dashboard/app/views/repo.js +205 -67
- package/dist/dashboard/app/views/workforce.js +65 -5
- package/dist/dashboard/routes/graph.d.ts +0 -11
- package/dist/dashboard/routes/graph.js +232 -54
- package/dist/dashboard/routes/runtime.js +52 -10
- package/dist/dashboard/selectors/summary-selector.js +22 -2
- package/dist/dashboard/server.d.ts +1 -0
- package/dist/dashboard/server.js +49 -7
- package/dist/engines/workspace-resolver.js +18 -1
- package/package.json +1 -1
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
* Renders a d3 force-layout combining:
|
|
5
5
|
* - File nodes (source / test / config / other) from /api/knowledge-topology
|
|
6
6
|
* - Memory nodes linked to those files
|
|
7
|
-
* -
|
|
7
|
+
* - Deterministic structural edges plus separate memory overlay edges
|
|
8
8
|
*
|
|
9
9
|
* Interactions:
|
|
10
10
|
* - Hover node → highlight 1-hop neighbours, dim others
|
|
11
11
|
* - Click node → open drawer with detail
|
|
12
12
|
* - Search input → highlight matching nodes by label
|
|
13
|
-
* - Build button → POST /api/
|
|
13
|
+
* - Build button → POST /api/knowledge-topology/build with live SSE status
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { S }
|
|
17
|
-
import { api, post }
|
|
18
|
-
import { openDrawer }
|
|
16
|
+
import { S } from '../state.js';
|
|
17
|
+
import { api, post, bustCache } from '../api.js';
|
|
18
|
+
import { openDrawer } from '../widgets/drawer.js';
|
|
19
19
|
|
|
20
20
|
const $ = id => document.getElementById(id);
|
|
21
21
|
const esc = s => s == null ? '' : String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
@@ -23,11 +23,15 @@ const esc = s => s == null ? '' : String(s).replace(/&/g,'&').replace(/</g,'
|
|
|
23
23
|
let _sim = null;
|
|
24
24
|
let _topo = null;
|
|
25
25
|
let _searchTerm = '';
|
|
26
|
+
let _buildState = { status: 'idle', message: '', at: 0 };
|
|
26
27
|
|
|
27
28
|
/* ── Data loader ── */
|
|
28
29
|
export async function load() {
|
|
29
30
|
_topo = await api('/api/knowledge-topology', 60_000);
|
|
30
31
|
if (_topo?.error) _topo = null;
|
|
32
|
+
if (_topo?.nodes?.length && !['queued', 'building'].includes(_buildState.status)) {
|
|
33
|
+
_setBuildState('ready', 'Graph ready.');
|
|
34
|
+
}
|
|
31
35
|
_renderMeta();
|
|
32
36
|
_renderGraph();
|
|
33
37
|
_bindControls();
|
|
@@ -38,34 +42,84 @@ export function render() {
|
|
|
38
42
|
_renderGraph();
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
export function handleBuildEvent(evt) {
|
|
46
|
+
const repoRoot = evt?.payload?.repoRoot;
|
|
47
|
+
const selectedRoot = S.workspace?.repoRoot;
|
|
48
|
+
if (repoRoot && selectedRoot && repoRoot !== selectedRoot) return;
|
|
49
|
+
|
|
50
|
+
if (evt?.type === 'graph.cr.build.start') {
|
|
51
|
+
_setBuildState('building', 'Graph build in progress…');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (evt?.type === 'graph.cr.build.complete') {
|
|
55
|
+
_setBuildState('ready', 'Graph ready.');
|
|
56
|
+
bustCache('/api/knowledge-topology');
|
|
57
|
+
void load();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (evt?.type === 'graph.cr.build.failed') {
|
|
61
|
+
_setBuildState('failed', 'Graph build failed. Check backend logs and retry.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
41
65
|
/* ── Meta header ── */
|
|
42
66
|
function _renderMeta() {
|
|
43
67
|
const el = $('repo-graph-meta'); if (!el) return;
|
|
44
|
-
if (!_topo) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
68
|
+
if (!_topo) {
|
|
69
|
+
el.textContent = _buildState.message || 'No topology data';
|
|
70
|
+
_syncBuildControls();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const meta = _topo.meta || {};
|
|
74
|
+
const provenance = _topo.provenance || {};
|
|
75
|
+
const generatedAt = _topo.generatedAt
|
|
76
|
+
? new Date(_topo.generatedAt).toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' })
|
|
77
|
+
: '—';
|
|
78
|
+
const buildLabel = _buildState.status === 'idle'
|
|
79
|
+
? 'idle'
|
|
80
|
+
: `${_buildState.status}${_buildState.message ? ` · ${_buildState.message}` : ''}`;
|
|
81
|
+
el.innerHTML = [
|
|
82
|
+
`<span>${esc(_topo.repoName || 'workspace')}</span>`,
|
|
83
|
+
`<span>${fmtNum(meta.fileCount ?? 0)} files</span>`,
|
|
84
|
+
`<span>${fmtNum(meta.edgeCount ?? 0)} edges</span>`,
|
|
85
|
+
`<span>${esc(provenance.graphSource || meta.graphSource || 'unknown-source')}</span>`,
|
|
86
|
+
`<span>${esc(provenance.freshness || meta.freshness || 'n/a')}</span>`,
|
|
87
|
+
`<span>${esc(buildLabel)}</span>`,
|
|
88
|
+
`<span>${esc(generatedAt)}</span>`,
|
|
89
|
+
].join(' · ');
|
|
90
|
+
_syncBuildControls();
|
|
48
91
|
}
|
|
49
92
|
|
|
50
93
|
/* ── D3 force graph ── */
|
|
51
94
|
function _renderGraph() {
|
|
52
95
|
const svg = $('repo-graph-svg');
|
|
53
96
|
const empty = $('repo-graph-empty');
|
|
54
|
-
if (!svg ||
|
|
97
|
+
if (!svg || !empty) return;
|
|
98
|
+
|
|
99
|
+
if (typeof d3 === 'undefined') {
|
|
100
|
+
svg.innerHTML = '';
|
|
101
|
+
_showEmpty('Graph rendering unavailable', 'D3 did not load. Build state and metadata are still live.');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
55
104
|
|
|
56
105
|
if (!_topo || !_topo.nodes?.length) {
|
|
57
|
-
|
|
106
|
+
svg.innerHTML = '';
|
|
107
|
+
const fallback = _buildState.status === 'failed'
|
|
108
|
+
? ['Graph build failed', _buildState.message || 'Retry the build when the backend is healthy.']
|
|
109
|
+
: _buildState.status === 'queued' || _buildState.status === 'building'
|
|
110
|
+
? ['Graph build in progress', _buildState.message || 'Waiting for backend build events…']
|
|
111
|
+
: ['Repo graph not built yet', 'Queue a graph build to render deterministic repo relationships.'];
|
|
112
|
+
_showEmpty(fallback[0], fallback[1]);
|
|
58
113
|
return;
|
|
59
114
|
}
|
|
60
|
-
|
|
115
|
+
|
|
116
|
+
empty.style.display = 'none';
|
|
61
117
|
|
|
62
118
|
const container = $('repo-graph-container');
|
|
63
119
|
const W = container?.clientWidth || 680;
|
|
64
120
|
const H = container?.clientHeight || 480;
|
|
65
121
|
|
|
66
|
-
// Build node and link arrays (mutable copies for simulation)
|
|
67
122
|
const nodes = _topo.nodes.map(n => ({ ...n }));
|
|
68
|
-
const nodeById = new Map(nodes.map(n => [n.id, n]));
|
|
69
123
|
const links = (_topo.edges || []).map(e => ({ ...e }));
|
|
70
124
|
|
|
71
125
|
const d3s = d3.select(svg);
|
|
@@ -75,38 +129,34 @@ function _renderGraph() {
|
|
|
75
129
|
if (_sim) _sim.stop();
|
|
76
130
|
|
|
77
131
|
_sim = d3.forceSimulation(nodes)
|
|
78
|
-
.force('link', d3.forceLink(links).id(d => d.id).distance(d => d.type
|
|
79
|
-
.force('charge', d3.forceManyBody().strength(d => d.type === 'memory' ? -40 : -
|
|
132
|
+
.force('link', d3.forceLink(links).id(d => d.id).distance(d => _edgeDistance(d.type)).strength(0.22))
|
|
133
|
+
.force('charge', d3.forceManyBody().strength(d => d.type === 'memory' ? -40 : -70))
|
|
80
134
|
.force('center', d3.forceCenter(W / 2, H / 2))
|
|
81
135
|
.force('collide', d3.forceCollide(d => _nodeR(d) + 3));
|
|
82
136
|
|
|
83
|
-
// ── Edge layer ──
|
|
84
137
|
const linkLayer = d3s.append('g').attr('class', 'links');
|
|
85
138
|
const linkEl = linkLayer.selectAll('line')
|
|
86
139
|
.data(links).enter().append('line')
|
|
87
|
-
.attr('stroke', d => d.type
|
|
88
|
-
.attr('stroke-width', 1)
|
|
89
|
-
.attr('stroke-opacity', 0.5)
|
|
90
|
-
.attr('stroke-dasharray', d =>
|
|
140
|
+
.attr('stroke', d => _edgeColor(d.type))
|
|
141
|
+
.attr('stroke-width', d => d.type === 'imports' ? 1.3 : 1)
|
|
142
|
+
.attr('stroke-opacity', d => d.type === 'memory-overlay' ? 0.35 : 0.5)
|
|
143
|
+
.attr('stroke-dasharray', d => ['memory-overlay', 'memory-association'].includes(d.type) ? '3,3' : null);
|
|
91
144
|
|
|
92
|
-
// ── Node layer ──
|
|
93
145
|
const nodeLayer = d3s.append('g').attr('class', 'nodes');
|
|
94
146
|
const nodeEl = nodeLayer.selectAll('.repo-node')
|
|
95
147
|
.data(nodes).enter().append(d => _makeNodeEl(d))
|
|
96
148
|
.attr('class', 'repo-node')
|
|
97
149
|
.style('cursor', 'pointer')
|
|
98
|
-
.on('mouseover', (ev, d) => _onHover(d, nodeEl, linkEl
|
|
150
|
+
.on('mouseover', (ev, d) => _onHover(d, nodeEl, linkEl))
|
|
99
151
|
.on('mouseout', () => _onHoverOut(nodeEl, linkEl))
|
|
100
152
|
.on('click', (ev, d) => _onClick(d));
|
|
101
153
|
|
|
102
|
-
// Drag behaviour
|
|
103
154
|
const drag = d3.drag()
|
|
104
155
|
.on('start', (ev, d) => { if (!ev.active) _sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
105
156
|
.on('drag', (ev, d) => { d.fx = ev.x; d.fy = ev.y; })
|
|
106
157
|
.on('end', (ev, d) => { if (!ev.active) _sim.alphaTarget(0); d.fx = null; d.fy = null; });
|
|
107
158
|
nodeEl.call(drag);
|
|
108
159
|
|
|
109
|
-
// Tick
|
|
110
160
|
_sim.on('tick', () => {
|
|
111
161
|
linkEl
|
|
112
162
|
.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
|
@@ -125,26 +175,25 @@ function _makeNodeEl(d) {
|
|
|
125
175
|
c.setAttribute('r', String(r));
|
|
126
176
|
c.setAttribute('fill', _memColor(d));
|
|
127
177
|
c.setAttribute('fill-opacity', '0.85');
|
|
128
|
-
c.setAttribute('stroke', '#000');
|
|
129
|
-
c.setAttribute('stroke-width', '1');
|
|
130
|
-
if (d.promoted)
|
|
131
|
-
c.classList.add('node-pulse');
|
|
132
|
-
}
|
|
178
|
+
c.setAttribute('stroke', d.promoted ? '#a78bfa' : '#000');
|
|
179
|
+
c.setAttribute('stroke-width', d.promoted ? '1.5' : '1');
|
|
180
|
+
if (d.promoted) c.classList.add('node-pulse');
|
|
133
181
|
doc.appendChild(c);
|
|
134
|
-
|
|
135
|
-
const half = r;
|
|
136
|
-
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
137
|
-
rect.setAttribute('x', String(-half));
|
|
138
|
-
rect.setAttribute('y', String(-half));
|
|
139
|
-
rect.setAttribute('width', String(half * 2));
|
|
140
|
-
rect.setAttribute('height', String(half * 2));
|
|
141
|
-
rect.setAttribute('rx', '2');
|
|
142
|
-
rect.setAttribute('fill', _fileColor(d));
|
|
143
|
-
rect.setAttribute('fill-opacity', '0.8');
|
|
144
|
-
rect.setAttribute('stroke', '#000');
|
|
145
|
-
rect.setAttribute('stroke-width', '1');
|
|
146
|
-
doc.appendChild(rect);
|
|
182
|
+
return doc;
|
|
147
183
|
}
|
|
184
|
+
|
|
185
|
+
const half = r;
|
|
186
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
187
|
+
rect.setAttribute('x', String(-half));
|
|
188
|
+
rect.setAttribute('y', String(-half));
|
|
189
|
+
rect.setAttribute('width', String(half * 2));
|
|
190
|
+
rect.setAttribute('height', String(half * 2));
|
|
191
|
+
rect.setAttribute('rx', '2');
|
|
192
|
+
rect.setAttribute('fill', _fileColor(d));
|
|
193
|
+
rect.setAttribute('fill-opacity', '0.8');
|
|
194
|
+
rect.setAttribute('stroke', '#000');
|
|
195
|
+
rect.setAttribute('stroke-width', '1');
|
|
196
|
+
doc.appendChild(rect);
|
|
148
197
|
return doc;
|
|
149
198
|
}
|
|
150
199
|
|
|
@@ -153,11 +202,28 @@ function _nodeR(d) {
|
|
|
153
202
|
return 5;
|
|
154
203
|
}
|
|
155
204
|
|
|
205
|
+
function _edgeDistance(type) {
|
|
206
|
+
if (type === 'memory-association') return 85;
|
|
207
|
+
if (type === 'memory-overlay') return 70;
|
|
208
|
+
if (type === 'configures') return 80;
|
|
209
|
+
if (type === 'tests') return 60;
|
|
210
|
+
return 55;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function _edgeColor(type) {
|
|
214
|
+
if (type === 'imports') return '#27272a';
|
|
215
|
+
if (type === 'tests') return '#22c55e';
|
|
216
|
+
if (type === 'configures') return '#78716c';
|
|
217
|
+
if (type === 'memory-overlay') return '#00d4ff';
|
|
218
|
+
if (type === 'memory-association') return '#3f3f46';
|
|
219
|
+
return '#27272a';
|
|
220
|
+
}
|
|
221
|
+
|
|
156
222
|
function _memColor(d) {
|
|
157
223
|
const tier = d.tier || 'cortex';
|
|
158
224
|
if (tier === 'prefrontal' || tier === 'working') return '#00d4ff';
|
|
159
225
|
if (tier === 'hippocampus' || tier === 'episodic') return '#00ff88';
|
|
160
|
-
return '#ffd14d';
|
|
226
|
+
return '#ffd14d';
|
|
161
227
|
}
|
|
162
228
|
|
|
163
229
|
function _fileColor(d) {
|
|
@@ -169,7 +235,7 @@ function _fileColor(d) {
|
|
|
169
235
|
}
|
|
170
236
|
|
|
171
237
|
/* ── Hover: highlight 1-hop neighbours ── */
|
|
172
|
-
function _onHover(d, nodeEl, linkEl
|
|
238
|
+
function _onHover(d, nodeEl, linkEl) {
|
|
173
239
|
const connected = new Set([d.id]);
|
|
174
240
|
linkEl.each(function(l) {
|
|
175
241
|
const s = typeof l.source === 'object' ? l.source.id : l.source;
|
|
@@ -188,41 +254,55 @@ function _onHover(d, nodeEl, linkEl, nodeById) {
|
|
|
188
254
|
|
|
189
255
|
function _onHoverOut(nodeEl, linkEl) {
|
|
190
256
|
nodeEl.attr('opacity', 1);
|
|
191
|
-
linkEl.attr('stroke-opacity', 0.5);
|
|
257
|
+
linkEl.attr('stroke-opacity', l => l.type === 'memory-overlay' ? 0.35 : 0.5);
|
|
192
258
|
_hideTooltip();
|
|
193
259
|
}
|
|
194
260
|
|
|
195
261
|
/* ── Click → drawer ── */
|
|
196
262
|
function _onClick(d) {
|
|
197
|
-
const
|
|
263
|
+
const rows = [];
|
|
198
264
|
if (d.type === 'file') {
|
|
199
|
-
|
|
200
|
-
|
|
265
|
+
rows.push(['Path', d.path]);
|
|
266
|
+
rows.push(['Kind', d.kind || 'other']);
|
|
201
267
|
} else {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
lines.push(`<div class="detail-row"><span class="detail-lbl">Tags</span>${d.tags.map(t=>`<span class="tag">${esc(t)}</span>`).join(' ')}</div>`);
|
|
206
|
-
}
|
|
207
|
-
lines.push(`<div class="detail-row" style="margin-top:8px">${esc(d.label)}</div>`);
|
|
268
|
+
rows.push(['Tier', d.tier || 'cortex']);
|
|
269
|
+
rows.push(['Priority', (d.priority || 0).toFixed(2)]);
|
|
270
|
+
rows.push(['Tags', Array.isArray(d.tags) && d.tags.length ? d.tags.join(', ') : '—']);
|
|
208
271
|
}
|
|
209
|
-
|
|
272
|
+
rows.push(['Node type', d.type || 'unknown']);
|
|
273
|
+
openDrawer({
|
|
274
|
+
title: d.label || d.id,
|
|
275
|
+
body: `<div class="detail-block">
|
|
276
|
+
${rows.map(([k, v]) => `<div class="detail-row"><span class="detail-lbl">${esc(k)}</span><span>${esc(String(v ?? '—'))}</span></div>`).join('')}
|
|
277
|
+
</div>`,
|
|
278
|
+
});
|
|
210
279
|
}
|
|
211
280
|
|
|
212
281
|
/* ── Search highlight ── */
|
|
213
282
|
function _applySearch() {
|
|
214
|
-
|
|
283
|
+
const nodes = d3.selectAll('.repo-node');
|
|
284
|
+
if (!_searchTerm) {
|
|
285
|
+
nodes.attr('opacity', 1);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
215
288
|
const q = _searchTerm.toLowerCase();
|
|
216
|
-
|
|
217
|
-
(d.label || d.path || d.id || '').toLowerCase().includes(q) ? 1 : 0.15,
|
|
218
|
-
);
|
|
289
|
+
nodes.attr('opacity', d => (d.label || d.path || d.id || '').toLowerCase().includes(q) ? 1 : 0.15);
|
|
219
290
|
}
|
|
220
291
|
|
|
221
292
|
/* ── Controls ── */
|
|
222
293
|
function _bindControls() {
|
|
223
294
|
const buildHandler = async () => {
|
|
224
|
-
|
|
295
|
+
_setBuildState('queued', 'Graph build queued…');
|
|
296
|
+
const result = await post('/api/knowledge-topology/build', {});
|
|
297
|
+
if (!result.ok) {
|
|
298
|
+
_setBuildState('failed', result.error || 'Graph build could not be queued.');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
bustCache('/api/knowledge-topology');
|
|
302
|
+
_renderMeta();
|
|
303
|
+
_renderGraph();
|
|
225
304
|
};
|
|
305
|
+
|
|
226
306
|
const b1 = $('repo-build-btn');
|
|
227
307
|
const b2 = $('repo-build-btn2');
|
|
228
308
|
if (b1) b1.onclick = buildHandler;
|
|
@@ -232,13 +312,58 @@ function _bindControls() {
|
|
|
232
312
|
if (si) {
|
|
233
313
|
si.oninput = () => {
|
|
234
314
|
_searchTerm = si.value.trim();
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
} else {
|
|
238
|
-
_applySearch();
|
|
239
|
-
}
|
|
315
|
+
if (typeof d3 === 'undefined') return;
|
|
316
|
+
_applySearch();
|
|
240
317
|
};
|
|
241
318
|
}
|
|
319
|
+
|
|
320
|
+
_syncBuildControls();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function _syncBuildControls() {
|
|
324
|
+
const buttons = [$('repo-build-btn'), $('repo-build-btn2')].filter(Boolean);
|
|
325
|
+
const label = _buildState.status === 'building'
|
|
326
|
+
? 'Building…'
|
|
327
|
+
: _buildState.status === 'queued'
|
|
328
|
+
? 'Queued…'
|
|
329
|
+
: _buildState.status === 'failed'
|
|
330
|
+
? 'Retry build'
|
|
331
|
+
: _buildState.status === 'ready'
|
|
332
|
+
? 'Rebuild graph'
|
|
333
|
+
: 'Build graph';
|
|
334
|
+
for (const btn of buttons) {
|
|
335
|
+
btn.textContent = label;
|
|
336
|
+
btn.disabled = ['queued', 'building'].includes(_buildState.status);
|
|
337
|
+
btn.title = _buildState.message || '';
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function _setBuildState(status, message='') {
|
|
342
|
+
_buildState = { status, message, at: Date.now() };
|
|
343
|
+
_syncBuildControls();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function _showEmpty(title, subtitle) {
|
|
347
|
+
const empty = $('repo-graph-empty');
|
|
348
|
+
if (!empty) return;
|
|
349
|
+
empty.style.display = 'flex';
|
|
350
|
+
empty.innerHTML = `
|
|
351
|
+
<span>${esc(title)}</span>
|
|
352
|
+
<span style="font-size:var(--caption);opacity:.7;max-width:360px;text-align:center">${esc(subtitle)}</span>
|
|
353
|
+
<button id="repo-build-btn2" class="btn btn-sm">${_buildState.status === 'failed' ? 'Retry build' : 'Build now'}</button>`;
|
|
354
|
+
$('repo-build-btn2')?.addEventListener('click', async () => {
|
|
355
|
+
_setBuildState('queued', 'Graph build queued…');
|
|
356
|
+
const result = await post('/api/knowledge-topology/build', {});
|
|
357
|
+
if (!result.ok) {
|
|
358
|
+
_setBuildState('failed', result.error || 'Graph build could not be queued.');
|
|
359
|
+
_renderMeta();
|
|
360
|
+
_renderGraph();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
bustCache('/api/knowledge-topology');
|
|
364
|
+
_renderMeta();
|
|
365
|
+
_renderGraph();
|
|
366
|
+
});
|
|
242
367
|
}
|
|
243
368
|
|
|
244
369
|
/* ── Tooltip ── */
|
|
@@ -253,10 +378,23 @@ function _showTooltip(txt) {
|
|
|
253
378
|
_tip.style.display = 'block';
|
|
254
379
|
document.addEventListener('mousemove', _moveTooltip, { passive: true });
|
|
255
380
|
}
|
|
381
|
+
|
|
256
382
|
function _moveTooltip(ev) {
|
|
257
|
-
if (_tip) {
|
|
383
|
+
if (_tip) {
|
|
384
|
+
_tip.style.left = (ev.clientX + 12) + 'px';
|
|
385
|
+
_tip.style.top = (ev.clientY - 8) + 'px';
|
|
386
|
+
}
|
|
258
387
|
}
|
|
388
|
+
|
|
259
389
|
function _hideTooltip() {
|
|
260
390
|
if (_tip) _tip.style.display = 'none';
|
|
261
391
|
document.removeEventListener('mousemove', _moveTooltip);
|
|
262
392
|
}
|
|
393
|
+
|
|
394
|
+
function fmtNum(n) {
|
|
395
|
+
if (n == null || isNaN(n)) return '—';
|
|
396
|
+
const v = Number(n);
|
|
397
|
+
if (v >= 1e6) return (v / 1e6).toFixed(1) + 'M';
|
|
398
|
+
if (v >= 1000) return (v / 1000).toFixed(1) + 'K';
|
|
399
|
+
return String(Math.round(v));
|
|
400
|
+
}
|
|
@@ -134,19 +134,22 @@ function _buildDispatchStrip(run) {
|
|
|
134
134
|
|
|
135
135
|
/* ── Data loader ── */
|
|
136
136
|
export async function load() {
|
|
137
|
-
const [teams, health, disp, appr, assets] = await Promise.all([
|
|
137
|
+
const [teams, health, disp, appr, assets, healthData] = await Promise.all([
|
|
138
138
|
api('/api/synapse/teams', 5000),
|
|
139
139
|
api('/api/synapse/health', 5000),
|
|
140
140
|
api('/api/architects/dispatch', 5000),
|
|
141
141
|
api('/api/synapse/approvals', 5000),
|
|
142
142
|
api('/api/dashboard/surface/assets', 15000),
|
|
143
|
+
api('/api/health', 15000),
|
|
143
144
|
]);
|
|
144
145
|
S.synapseTeams = teams;
|
|
146
|
+
S.synapseHealthRaw = health;
|
|
145
147
|
S.synapseHealth = (Array.isArray(health) ? health : (health?.operatives||[])).map(_norm);
|
|
146
148
|
S.archDispatch = disp;
|
|
147
149
|
S.approvals = Array.isArray(appr) ? appr : (appr?.approvals||[]);
|
|
148
150
|
S.assetsSurface = assets;
|
|
149
|
-
|
|
151
|
+
S.healthData = healthData;
|
|
152
|
+
notifyNotReady([teams, health, disp]);
|
|
150
153
|
render();
|
|
151
154
|
}
|
|
152
155
|
|
|
@@ -303,26 +306,73 @@ function _buildHireSelectors() {
|
|
|
303
306
|
teamOptions: sel('hire-team', 'Strike team (optional)', teamOptions) };
|
|
304
307
|
}
|
|
305
308
|
|
|
309
|
+
function _getHireReadiness() {
|
|
310
|
+
const synapseState = S.healthData?.runtimeEnvelope?.engines?.synapse ?? {};
|
|
311
|
+
const memoryStorage = S.healthData?.memory?.storage ?? {};
|
|
312
|
+
const notes = [];
|
|
313
|
+
const blocked = Boolean(
|
|
314
|
+
S.synapseHealthRaw?.notReady
|
|
315
|
+
|| synapseState.ready === false
|
|
316
|
+
|| synapseState.available === false,
|
|
317
|
+
);
|
|
318
|
+
if (blocked) {
|
|
319
|
+
notes.push({ tone: 'bad', text: S.synapseHealthRaw?.reason || synapseState.reason || 'Synapse is not ready for hires in this dashboard session.' });
|
|
320
|
+
}
|
|
321
|
+
if (synapseState.fallbackApplied) {
|
|
322
|
+
notes.push({ tone: 'warn', text: synapseState.reason || 'Synapse is running in fallback storage mode.' });
|
|
323
|
+
}
|
|
324
|
+
if (memoryStorage.fallbackApplied) {
|
|
325
|
+
notes.push({ tone: 'warn', text: memoryStorage.fallbackReason || 'Memory storage fallback is active.' });
|
|
326
|
+
}
|
|
327
|
+
return { blocked, notes };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function _formatHireFailure(result) {
|
|
331
|
+
const hint = result?.data?.hint;
|
|
332
|
+
const base = result?.error || 'Hire failed.';
|
|
333
|
+
return hint ? `${base} ${hint}` : base;
|
|
334
|
+
}
|
|
335
|
+
|
|
306
336
|
function _showHireSheet(specialistId, name) {
|
|
307
337
|
const { opOptions, teamOptions } = _buildHireSelectors();
|
|
338
|
+
const readiness = _getHireReadiness();
|
|
339
|
+
const noticesHtml = readiness.notes.length
|
|
340
|
+
? `<div class="dsec" style="margin-bottom:var(--space-4)">
|
|
341
|
+
${readiness.notes.map(note => `<div style="padding:8px 10px;border:1px solid ${note.tone === 'bad' ? '#ff5f5733' : '#ffd14d33'};background:${note.tone === 'bad' ? '#ff5f5712' : '#ffd14d12'};color:${note.tone === 'bad' ? 'var(--bad)' : '#ffd14d'};border-radius:8px;font-size:var(--text-sm);margin-bottom:8px">${esc(note.text)}</div>`).join('')}
|
|
342
|
+
</div>`
|
|
343
|
+
: '';
|
|
308
344
|
openDrawer({ title: `Hire ${name}`,
|
|
309
345
|
body: `<div class="dsec">
|
|
310
346
|
<div class="dsec-title">Specialist</div>
|
|
311
347
|
<div style="font-size:var(--text-sm);color:var(--text-muted);margin-bottom:var(--space-4)">${esc(name)}<br><span style="opacity:.6">${esc(specialistId)}</span></div>
|
|
348
|
+
${noticesHtml}
|
|
312
349
|
${opOptions}
|
|
313
350
|
${teamOptions}
|
|
314
351
|
<label style="display:block;margin-bottom:var(--space-2);font-size:var(--text-sm);color:var(--text-muted)">Budget cap (USD)</label>
|
|
315
352
|
<input type="number" id="hire-budget" value="2.00" step="0.50" min="0.50" max="50"
|
|
316
353
|
style="width:100%;background:var(--bg-panel);border:1px solid var(--border);border-radius:var(--radius);padding:6px 10px;font-size:var(--text-sm);color:var(--text-main);margin-bottom:var(--space-4)">
|
|
317
|
-
<button class="btn btn-primary" id="hire-confirm-btn" data-specid="${esc(specialistId)}" data-specname="${esc(name)}">Confirm Hire</button>
|
|
354
|
+
<button class="btn btn-primary" id="hire-confirm-btn" data-specid="${esc(specialistId)}" data-specname="${esc(name)}" ${readiness.blocked ? 'disabled title="Synapse is not ready"' : ''}>Confirm Hire</button>
|
|
355
|
+
<div id="hire-inline-status" style="margin-top:var(--space-3);font-size:var(--text-sm);color:${readiness.blocked ? 'var(--bad)' : 'var(--text-muted)'}">${readiness.blocked ? esc(readiness.notes[0]?.text || 'Synapse is not ready for hires.') : ''}</div>
|
|
318
356
|
</div>` });
|
|
319
357
|
|
|
320
358
|
document.getElementById('hire-confirm-btn')?.addEventListener('click', async btn => {
|
|
359
|
+
if (readiness.blocked) {
|
|
360
|
+
const status = document.getElementById('hire-inline-status');
|
|
361
|
+
if (status) status.textContent = readiness.notes[0]?.text || 'Synapse is not ready for hires.';
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
321
364
|
const specId = btn.target.dataset.specid;
|
|
322
365
|
const specName = btn.target.dataset.specname;
|
|
323
366
|
const budget = parseFloat(document.getElementById('hire-budget')?.value||'2');
|
|
324
367
|
const reportsToVal = (document.getElementById('hire-reports-to')?.value||'').trim()||null;
|
|
325
368
|
const teamVal = (document.getElementById('hire-team')?.value||'').trim()||null;
|
|
369
|
+
const inlineStatus = document.getElementById('hire-inline-status');
|
|
370
|
+
if (inlineStatus) {
|
|
371
|
+
inlineStatus.textContent = readiness.notes.some(note => note.tone === 'warn')
|
|
372
|
+
? 'Submitting hire request. Fallback storage warning is active.'
|
|
373
|
+
: 'Submitting hire request…';
|
|
374
|
+
inlineStatus.style.color = readiness.notes.some(note => note.tone === 'warn') ? '#ffd14d' : 'var(--text-muted)';
|
|
375
|
+
}
|
|
326
376
|
btn.target.disabled = true;
|
|
327
377
|
btn.target.textContent = 'Hiring…';
|
|
328
378
|
|
|
@@ -341,13 +391,20 @@ function _showHireSheet(specialistId, name) {
|
|
|
341
391
|
<div style="color:var(--accent);font-size:var(--text-sm);margin-bottom:var(--space-4)" id="hire-status-msg">Confirming with server…</div>
|
|
342
392
|
<div class="drow" id="hire-id-row" style="display:none"><span class="drow-k">ID</span><span class="drow-v" id="hire-id-val">—</span></div>
|
|
343
393
|
<div class="drow" id="hire-cost-row" style="display:none"><span class="drow-k">Est. per sortie</span><span class="drow-v" id="hire-cost-val">—</span></div>
|
|
394
|
+
<div class="drow"><span class="drow-k">Reports to</span><span class="drow-v">${esc(reportsToVal || 'team lead')}</span></div>
|
|
395
|
+
<div class="drow"><span class="drow-k">Strike team</span><span class="drow-v">${esc(teamVal || 'solo')}</span></div>
|
|
396
|
+
<div class="drow"><span class="drow-k">Budget cap</span><span class="drow-v">$${budget.toFixed(2)}</span></div>
|
|
344
397
|
</div>` });
|
|
345
398
|
|
|
346
399
|
// Reconcile when real response arrives.
|
|
347
400
|
result.realResponse?.then(real => {
|
|
348
401
|
const msg = document.getElementById('hire-status-msg');
|
|
349
402
|
if (real.ok) {
|
|
350
|
-
if (msg)
|
|
403
|
+
if (msg) {
|
|
404
|
+
msg.textContent = readiness.notes.some(note => note.tone === 'warn')
|
|
405
|
+
? 'Hired successfully. Fallback storage warning is still active.'
|
|
406
|
+
: 'Hired successfully.';
|
|
407
|
+
}
|
|
351
408
|
const idRow = document.getElementById('hire-id-row');
|
|
352
409
|
const idVal = document.getElementById('hire-id-val');
|
|
353
410
|
if (idRow && idVal) { idVal.textContent = (real.data?.operative?.id||'').slice(-12)||'—'; idRow.style.display=''; }
|
|
@@ -393,7 +450,10 @@ function _showHireSheet(specialistId, name) {
|
|
|
393
450
|
}).catch(() => { _dispatches.delete('__warmup__'); _refreshDrawerForOp(operativeId); });
|
|
394
451
|
}
|
|
395
452
|
} else {
|
|
396
|
-
if (msg) {
|
|
453
|
+
if (msg) {
|
|
454
|
+
msg.textContent = `Hire failed: ${_formatHireFailure(real)}`;
|
|
455
|
+
msg.style.color = 'var(--bad)';
|
|
456
|
+
}
|
|
397
457
|
}
|
|
398
458
|
}).catch(() => {
|
|
399
459
|
const msg = document.getElementById('hire-status-msg');
|
|
@@ -1,13 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* graph.ts — /api/knowledge-topology
|
|
3
|
-
*
|
|
4
|
-
* Returns a combined node+edge graph for the repo view:
|
|
5
|
-
* - File nodes from orchestrator.listTopologyFiles() (up to 120)
|
|
6
|
-
* - Memory nodes from memory.listSnapshots() (up to 60)
|
|
7
|
-
* - Edges: memory→file when a relative file path appears in a memory excerpt
|
|
8
|
-
*
|
|
9
|
-
* Cached 60s via respondCachedJson. No cr-graph connection at route time
|
|
10
|
-
* (cr-graph build is non-blocking, done by ensureCrGraphBuilt in Cut 18).
|
|
11
|
-
*/
|
|
12
1
|
import type { DashboardRouteHandler } from '../types.js';
|
|
13
2
|
export declare const handleGraphRoutes: DashboardRouteHandler;
|