nexus-prime 7.9.30 → 7.9.31
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/styles/board.css +17 -1
- package/dist/dashboard/app/styles/governance.css +93 -0
- package/dist/dashboard/app/styles/memory.css +81 -1
- package/dist/dashboard/app/styles/runtime.css +2 -2
- package/dist/dashboard/app/styles/workforce.css +57 -3
- package/dist/dashboard/app/views/board.js +64 -11
- package/dist/dashboard/app/views/governance.js +19 -11
- package/dist/dashboard/app/views/memory.js +167 -9
- package/dist/dashboard/app/views/runtime.js +93 -3
- package/dist/dashboard/app/views/workforce.js +154 -16
- package/package.json +1 -1
|
@@ -132,7 +132,21 @@
|
|
|
132
132
|
.kcard.dragging { opacity: 0.35; transform: scale(0.96); cursor: grabbing; pointer-events: none; }
|
|
133
133
|
|
|
134
134
|
/* ── Agents Live Strip ── */
|
|
135
|
-
#agents-live-strip { display: flex; flex-wrap: wrap; gap:
|
|
135
|
+
#agents-live-strip { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 10px; }
|
|
136
|
+
.agent-live-summary {
|
|
137
|
+
display: inline-flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
gap: 5px;
|
|
140
|
+
padding: 3px 8px;
|
|
141
|
+
border: 1px solid rgba(0,255,136,0.18);
|
|
142
|
+
border-radius: 20px;
|
|
143
|
+
background: rgba(0,255,136,0.05);
|
|
144
|
+
color: var(--text-dim);
|
|
145
|
+
font-family: var(--font-mono);
|
|
146
|
+
font-size: 0.7rem;
|
|
147
|
+
}
|
|
148
|
+
.agent-live-summary strong { color: var(--accent); }
|
|
149
|
+
.agent-live-summary .agent-live-warn { color: var(--warning); }
|
|
136
150
|
.agent-live-pill {
|
|
137
151
|
display: inline-flex; align-items: center; gap: 5px;
|
|
138
152
|
font-family: var(--font-mono); font-size: 0.78rem;
|
|
@@ -143,6 +157,8 @@
|
|
|
143
157
|
.agent-live-pill .dot { width: 5px; height: 5px; border-radius: 50%; background: var(--text-dim); flex-shrink: 0; }
|
|
144
158
|
.agent-live-pill.active .dot { background: var(--accent); box-shadow: 0 0 4px rgba(0,255,136,0.6); }
|
|
145
159
|
.agent-live-pill.active { border-color: rgba(0,255,136,0.25); }
|
|
160
|
+
.agent-live-pill.idle .dot { background: var(--text-dim); opacity: 0.8; }
|
|
161
|
+
.agent-live-pill.idle { border-color: rgba(255,255,255,0.08); opacity: 0.78; }
|
|
146
162
|
.agent-live-pill.blocked .dot { background: var(--warning); }
|
|
147
163
|
.agent-live-pill.blocked { border-color: rgba(255,95,87,0.25); }
|
|
148
164
|
.agent-inline-badge {
|
|
@@ -2,6 +2,74 @@
|
|
|
2
2
|
|
|
3
3
|
.governance-container { padding: 4px 0; }
|
|
4
4
|
|
|
5
|
+
.governance-status-grid {
|
|
6
|
+
display: grid;
|
|
7
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
8
|
+
gap: 12px;
|
|
9
|
+
max-width: 1120px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.governance-status-card {
|
|
13
|
+
padding: 18px 20px;
|
|
14
|
+
min-height: 150px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.governance-card-head {
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: space-between;
|
|
21
|
+
gap: 10px;
|
|
22
|
+
margin-bottom: 16px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.governance-card-title {
|
|
26
|
+
color: var(--text);
|
|
27
|
+
font-weight: var(--weight-semibold);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.governance-status-body {
|
|
31
|
+
display: grid;
|
|
32
|
+
gap: 9px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.governance-status-body.compact {
|
|
36
|
+
margin-top: 14px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.governance-status-row {
|
|
40
|
+
display: grid;
|
|
41
|
+
grid-template-columns: minmax(120px, 0.42fr) minmax(0, 1fr);
|
|
42
|
+
gap: 12px;
|
|
43
|
+
align-items: baseline;
|
|
44
|
+
padding-bottom: 8px;
|
|
45
|
+
border-bottom: 1px solid var(--border);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.governance-status-row:last-child {
|
|
49
|
+
border-bottom: none;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.governance-status-row span {
|
|
53
|
+
color: var(--text-muted);
|
|
54
|
+
font-size: var(--text-sm);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.governance-status-row strong {
|
|
58
|
+
min-width: 0;
|
|
59
|
+
color: var(--text);
|
|
60
|
+
font-family: var(--font-mono);
|
|
61
|
+
font-size: var(--text-sm);
|
|
62
|
+
overflow-wrap: anywhere;
|
|
63
|
+
text-align: right;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.governance-status-copy {
|
|
67
|
+
color: var(--text-muted);
|
|
68
|
+
font-size: var(--text-sm);
|
|
69
|
+
line-height: 1.5;
|
|
70
|
+
max-width: 420px;
|
|
71
|
+
}
|
|
72
|
+
|
|
5
73
|
/* Darwin proposal cards */
|
|
6
74
|
.darwin-card { margin-bottom: 14px; padding: 16px 18px; }
|
|
7
75
|
|
|
@@ -37,12 +105,21 @@
|
|
|
37
105
|
}
|
|
38
106
|
|
|
39
107
|
.darwin-metric {
|
|
108
|
+
display: inline-grid;
|
|
109
|
+
grid-template-columns: auto auto auto;
|
|
110
|
+
align-items: center;
|
|
111
|
+
gap: 6px;
|
|
40
112
|
font-family: var(--font-mono);
|
|
41
113
|
font-size: var(--caption);
|
|
42
114
|
padding: 2px 8px;
|
|
43
115
|
border-radius: var(--radius-sm);
|
|
44
116
|
background: var(--surface-2);
|
|
45
117
|
}
|
|
118
|
+
.darwin-metric strong,
|
|
119
|
+
.darwin-metric em {
|
|
120
|
+
font-style: normal;
|
|
121
|
+
font-weight: 600;
|
|
122
|
+
}
|
|
46
123
|
.darwin-metric.metric-up { color: var(--ok); }
|
|
47
124
|
.darwin-metric.metric-down { color: var(--bad); }
|
|
48
125
|
.darwin-metric.metric-flat { color: var(--text-muted); }
|
|
@@ -119,3 +196,19 @@
|
|
|
119
196
|
|
|
120
197
|
.btn-ok { color: var(--ok); border-color: var(--ok); }
|
|
121
198
|
.btn-bad { color: var(--bad); border-color: var(--bad); }
|
|
199
|
+
|
|
200
|
+
@media (max-width: 900px) {
|
|
201
|
+
.governance-status-grid {
|
|
202
|
+
grid-template-columns: 1fr;
|
|
203
|
+
}
|
|
204
|
+
.governance-status-row {
|
|
205
|
+
grid-template-columns: 1fr;
|
|
206
|
+
gap: 3px;
|
|
207
|
+
}
|
|
208
|
+
.governance-status-row strong {
|
|
209
|
+
text-align: left;
|
|
210
|
+
}
|
|
211
|
+
.darwin-metric {
|
|
212
|
+
grid-template-columns: 1fr;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
.memory-graph-hud {
|
|
50
50
|
position: absolute; top: 10px; left: 10px;
|
|
51
51
|
display: flex; flex-wrap: wrap; gap: 6px;
|
|
52
|
-
max-width: min(560px, calc(100% -
|
|
52
|
+
max-width: min(560px, calc(100% - 230px));
|
|
53
53
|
pointer-events: none;
|
|
54
54
|
z-index: 9;
|
|
55
55
|
}
|
|
@@ -63,6 +63,84 @@
|
|
|
63
63
|
font-size: 0.68rem;
|
|
64
64
|
white-space: nowrap;
|
|
65
65
|
}
|
|
66
|
+
.memory-graph-controls {
|
|
67
|
+
position: absolute;
|
|
68
|
+
right: 12px;
|
|
69
|
+
bottom: 12px;
|
|
70
|
+
z-index: 12;
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-wrap: wrap;
|
|
73
|
+
justify-content: flex-end;
|
|
74
|
+
gap: 6px;
|
|
75
|
+
max-width: min(360px, calc(100% - 24px));
|
|
76
|
+
padding: 6px;
|
|
77
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
78
|
+
border-radius: var(--radius);
|
|
79
|
+
background: rgba(0,0,0,0.58);
|
|
80
|
+
backdrop-filter: blur(10px);
|
|
81
|
+
}
|
|
82
|
+
.memory-graph-btn {
|
|
83
|
+
min-width: 32px;
|
|
84
|
+
height: 28px;
|
|
85
|
+
padding: 0 9px;
|
|
86
|
+
border: 1px solid var(--border);
|
|
87
|
+
border-radius: var(--radius);
|
|
88
|
+
background: rgba(12,12,14,0.86);
|
|
89
|
+
color: var(--text-muted);
|
|
90
|
+
font-family: var(--font-mono);
|
|
91
|
+
font-size: 0.7rem;
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
}
|
|
94
|
+
.memory-graph-btn:hover,
|
|
95
|
+
.memory-graph-btn:focus-visible {
|
|
96
|
+
border-color: rgba(0,255,136,0.35);
|
|
97
|
+
color: var(--accent);
|
|
98
|
+
outline: none;
|
|
99
|
+
}
|
|
100
|
+
.memory-graph-node-browser {
|
|
101
|
+
display: flex;
|
|
102
|
+
flex-direction: column;
|
|
103
|
+
gap: 6px;
|
|
104
|
+
}
|
|
105
|
+
.memory-graph-node-summary {
|
|
106
|
+
display: flex;
|
|
107
|
+
gap: 6px;
|
|
108
|
+
flex-wrap: wrap;
|
|
109
|
+
margin-bottom: 6px;
|
|
110
|
+
}
|
|
111
|
+
.memory-graph-node-row {
|
|
112
|
+
width: 100%;
|
|
113
|
+
display: grid;
|
|
114
|
+
grid-template-columns: 82px minmax(0, 1fr) auto;
|
|
115
|
+
gap: 8px;
|
|
116
|
+
align-items: center;
|
|
117
|
+
padding: 8px 10px;
|
|
118
|
+
border: 1px solid var(--border);
|
|
119
|
+
border-radius: var(--radius);
|
|
120
|
+
background: var(--bg-panel);
|
|
121
|
+
color: var(--text-main);
|
|
122
|
+
text-align: left;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
}
|
|
125
|
+
.memory-graph-node-row:hover,
|
|
126
|
+
.memory-graph-node-row:focus-visible {
|
|
127
|
+
border-color: rgba(0,255,136,0.28);
|
|
128
|
+
outline: none;
|
|
129
|
+
}
|
|
130
|
+
.memory-graph-node-type,
|
|
131
|
+
.memory-graph-node-links {
|
|
132
|
+
color: var(--text-dim);
|
|
133
|
+
font-family: var(--font-mono);
|
|
134
|
+
font-size: 0.66rem;
|
|
135
|
+
white-space: nowrap;
|
|
136
|
+
}
|
|
137
|
+
.memory-graph-node-title {
|
|
138
|
+
min-width: 0;
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
text-overflow: ellipsis;
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
font-size: 0.76rem;
|
|
143
|
+
}
|
|
66
144
|
.leg-item {
|
|
67
145
|
display: flex; align-items: center; gap: 6px;
|
|
68
146
|
font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-dim);
|
|
@@ -183,6 +261,8 @@
|
|
|
183
261
|
#repo-graph-container.graph-expanded { left: 16px; right: 16px; top: 56px; bottom: 16px; }
|
|
184
262
|
.repo-graph-actions { justify-content: flex-start; margin-left: 0; width: 100%; }
|
|
185
263
|
.repo-graph-controls { grid-template-columns: repeat(5, auto); }
|
|
264
|
+
.memory-graph-hud { max-width: calc(100% - 22px); top: 52px; }
|
|
265
|
+
.memory-graph-controls { left: 10px; right: 10px; justify-content: flex-start; }
|
|
186
266
|
.memory-bottom { grid-template-columns: 1fr; }
|
|
187
267
|
}
|
|
188
268
|
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
|
|
10
10
|
/* KPI row */
|
|
11
11
|
.runtime-kpis {
|
|
12
|
-
display:
|
|
12
|
+
display: grid;
|
|
13
|
+
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
|
|
13
14
|
gap: 12px;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
.rt-kpi {
|
|
17
|
-
flex: 1;
|
|
18
18
|
background: var(--surface);
|
|
19
19
|
border: 1px solid var(--border);
|
|
20
20
|
border-radius: 8px;
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
display: flex; align-items: center; justify-content: center;
|
|
11
11
|
}
|
|
12
12
|
.org-box {
|
|
13
|
-
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: 7px;
|
|
16
|
+
padding: 8px 12px; background: var(--bg-panel); border: 1px solid var(--border);
|
|
14
17
|
border-radius: var(--radius); font-family: var(--font-mono); font-size: 0.78rem;
|
|
15
18
|
color: var(--text-main); white-space: nowrap; cursor: pointer; transition: border-color 0.15s;
|
|
16
19
|
}
|
|
@@ -24,10 +27,32 @@
|
|
|
24
27
|
.org-branch[data-team] > div > .org-box { border-color: rgba(0,212,255,0.25); }
|
|
25
28
|
.op-status-dot {
|
|
26
29
|
display: inline-block; width: 5px; height: 5px; border-radius: 50%;
|
|
27
|
-
|
|
30
|
+
background: var(--text-dim); vertical-align: middle; flex-shrink: 0;
|
|
28
31
|
}
|
|
29
32
|
.op-status-dot.active { background: var(--accent); }
|
|
30
33
|
.op-status-dot.dead { background: var(--warning); }
|
|
34
|
+
.org-op-text {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
align-items: flex-start;
|
|
38
|
+
gap: 1px;
|
|
39
|
+
min-width: 0;
|
|
40
|
+
}
|
|
41
|
+
.org-op-name {
|
|
42
|
+
max-width: 180px;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
text-overflow: ellipsis;
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
}
|
|
47
|
+
.org-op-meta {
|
|
48
|
+
max-width: 180px;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
text-overflow: ellipsis;
|
|
51
|
+
white-space: nowrap;
|
|
52
|
+
color: var(--text-dim);
|
|
53
|
+
font-size: 0.62rem;
|
|
54
|
+
text-transform: uppercase;
|
|
55
|
+
}
|
|
31
56
|
|
|
32
57
|
/* ── Missions / Dispatch ── */
|
|
33
58
|
.workforce-bottom { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
@@ -49,6 +74,12 @@
|
|
|
49
74
|
display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
50
75
|
gap: 10px; margin-bottom: 14px;
|
|
51
76
|
}
|
|
77
|
+
.spec-cost {
|
|
78
|
+
margin-top: 5px;
|
|
79
|
+
color: var(--text-dim);
|
|
80
|
+
font-family: var(--font-mono);
|
|
81
|
+
font-size: 0.68rem;
|
|
82
|
+
}
|
|
52
83
|
|
|
53
84
|
/* ── Trust / Diff Viewer ── */
|
|
54
85
|
.trust-layout { display: grid; grid-template-columns: 1fr 300px; gap: 12px; }
|
|
@@ -131,7 +162,30 @@
|
|
|
131
162
|
|
|
132
163
|
/* ── Unified workforce kanban ── */
|
|
133
164
|
.workforce-kanban { padding: 0; background: transparent; border: none !important; }
|
|
134
|
-
.kanban-meta {
|
|
165
|
+
.kanban-meta {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
gap: 8px;
|
|
169
|
+
flex-wrap: wrap;
|
|
170
|
+
font-size: 0.75rem;
|
|
171
|
+
color: var(--text-dim);
|
|
172
|
+
margin-bottom: 8px;
|
|
173
|
+
padding: 0 4px;
|
|
174
|
+
}
|
|
175
|
+
.kanban-summary {
|
|
176
|
+
display: flex;
|
|
177
|
+
gap: 5px;
|
|
178
|
+
flex-wrap: wrap;
|
|
179
|
+
}
|
|
180
|
+
.kanban-summary-chip {
|
|
181
|
+
border: 1px solid var(--border);
|
|
182
|
+
border-radius: 999px;
|
|
183
|
+
padding: 1px 7px;
|
|
184
|
+
background: var(--bg-panel);
|
|
185
|
+
color: var(--text-muted);
|
|
186
|
+
font-family: var(--font-mono);
|
|
187
|
+
font-size: 0.66rem;
|
|
188
|
+
}
|
|
135
189
|
.kanban-scroll { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 8px; }
|
|
136
190
|
.kanban-lane {
|
|
137
191
|
min-width: 220px; max-width: 280px; flex-shrink: 0;
|
|
@@ -67,13 +67,21 @@ function catColor(cat) {
|
|
|
67
67
|
}
|
|
68
68
|
function normalizeOperative(op) {
|
|
69
69
|
if (!op || typeof op !== 'object') return op;
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const displayName = op.name || op.displayName || op.roleTitle || op.role || op.specialistId || op.id || op.operativeId;
|
|
71
|
+
const rawStatus = op.state || op.healthState || op.status || 'IDLE';
|
|
72
|
+
return { ...op,
|
|
73
|
+
displayName,
|
|
74
|
+
role: op.role || op.roleTitle || op.specialistId || displayName || null,
|
|
75
|
+
status: String(rawStatus).toLowerCase(),
|
|
72
76
|
budget: op.budgetCapUsd??op.budget??null,
|
|
73
77
|
budgetUsed: op.spentUsd??op.budgetUsed??0,
|
|
74
78
|
team: op.strikeTeamId||op.team||op.strikeName||null };
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
function isOpActive(op) {
|
|
82
|
+
return ['active','running','wip','claimed'].includes(String(op?.status || '').toLowerCase());
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
/* ── Data loader ── */
|
|
78
86
|
export async function load() {
|
|
79
87
|
const settle = async (jobs) => (await Promise.allSettled(jobs)).map(r => r.status === 'fulfilled' ? r.value : null);
|
|
@@ -92,12 +100,14 @@ export async function load() {
|
|
|
92
100
|
api('/api/health', 15_000, { timeoutMs: 3_500 }),
|
|
93
101
|
api('/api/memory/health', 15_000, { timeoutMs: 4_000 }),
|
|
94
102
|
api('/api/runs?limit=12', 5_000, { timeoutMs: 3_500 }),
|
|
95
|
-
|
|
103
|
+
api('/api/workforce/kanban', 5_000, { timeoutMs: 3_500 }),
|
|
104
|
+
]).then(([opR, shR, healthR, memR, runsR, kanbanR]) => {
|
|
96
105
|
const op = opR.status === 'fulfilled' ? opR.value : null;
|
|
97
106
|
const sh = shR.status === 'fulfilled' ? shR.value : null;
|
|
98
107
|
const health = healthR.status === 'fulfilled' ? healthR.value : null;
|
|
99
108
|
const memHealth = memR.status === 'fulfilled' ? memR.value : null;
|
|
100
109
|
const runs = runsR.status === 'fulfilled' ? runsR.value : null;
|
|
110
|
+
const kanban = kanbanR.status === 'fulfilled' ? kanbanR.value : null;
|
|
101
111
|
if (op) S.operateSurface = op;
|
|
102
112
|
if (sh) {
|
|
103
113
|
S.synapseHealthRaw = sh;
|
|
@@ -107,6 +117,7 @@ export async function load() {
|
|
|
107
117
|
if (health) S.healthData = health;
|
|
108
118
|
if (memHealth) S.memHealth = memHealth;
|
|
109
119
|
if (Array.isArray(runs)) S.runs = runs;
|
|
120
|
+
if (kanban) S.wfKanban = kanban;
|
|
110
121
|
render();
|
|
111
122
|
});
|
|
112
123
|
// Prefetch curated specialists for first-run hero (non-blocking)
|
|
@@ -376,7 +387,7 @@ function renderHero() {
|
|
|
376
387
|
const pEl = $('m-pct');
|
|
377
388
|
if (pEl) { pEl.innerHTML=`${pct}<sup>%</sup>`; pEl.dataset.raw=pct; }
|
|
378
389
|
const memCount = op?.memorySummary?.total ?? op?.memory?.total ?? S.memHealth?.total ?? S.memories.length;
|
|
379
|
-
const opsCount = S.synapseHealth.
|
|
390
|
+
const opsCount = S.synapseHealth.length;
|
|
380
391
|
animCounter('m-memories', memCount);
|
|
381
392
|
animCounter('m-ops', opsCount);
|
|
382
393
|
if (!S.spark) S.spark = { tokens:[], pct:[], memories:[], ops:[] };
|
|
@@ -565,11 +576,18 @@ function renderAgentsLiveStrip() {
|
|
|
565
576
|
const strip = $('agents-live-strip'); if (!strip) return;
|
|
566
577
|
const ops = S.synapseHealth;
|
|
567
578
|
if (!ops.length) { strip.innerHTML=''; return; }
|
|
568
|
-
|
|
579
|
+
const active = ops.filter(isOpActive).length;
|
|
580
|
+
const failedJobs = (S.wfKanban?.lanes?.failed || []).length;
|
|
581
|
+
const summary = `<div class="agent-live-summary">
|
|
582
|
+
<strong>${fmtNum(ops.length)}</strong><span>hired</span>
|
|
583
|
+
<strong>${fmtNum(active)}</strong><span>active</span>
|
|
584
|
+
${failedJobs ? `<strong class="agent-live-warn">${fmtNum(failedJobs)}</strong><span>failed</span>` : ''}
|
|
585
|
+
</div>`;
|
|
586
|
+
strip.innerHTML = summary + ops.slice(0,12).map(op => {
|
|
569
587
|
const st = (op.status||'idle').toLowerCase();
|
|
570
|
-
const cls = (
|
|
571
|
-
const label = esc((op.
|
|
572
|
-
return `<div class="agent-live-pill ${cls}" data-opid="${esc(op.id)}" title="${label}">
|
|
588
|
+
const cls = isOpActive(op) ? 'active' : (st==='blocked'||st==='zombie'||st==='dead'||st==='failed') ? 'blocked' : 'idle';
|
|
589
|
+
const label = esc((op.displayName||op.name||op.role||op.id||'agent').slice(0,24));
|
|
590
|
+
return `<div class="agent-live-pill ${cls}" data-opid="${esc(op.id)}" title="${label} · ${esc(st)}">
|
|
573
591
|
<span class="dot"></span><span>${label}</span></div>`;
|
|
574
592
|
}).join('');
|
|
575
593
|
}
|
|
@@ -577,6 +595,7 @@ function renderAgentsLiveStrip() {
|
|
|
577
595
|
/* ── Kanban ── */
|
|
578
596
|
function buildKanbanCols() {
|
|
579
597
|
const cols={planning:[],hiring:[],running:[],ghostpass:[],done:[]};
|
|
598
|
+
const seen = new Set();
|
|
580
599
|
const op=S.operateSurface;
|
|
581
600
|
if (op) {
|
|
582
601
|
const pc=op.orchestration?.planningContext||op.planningContext;
|
|
@@ -588,6 +607,36 @@ function buildKanbanCols() {
|
|
|
588
607
|
if (sg) cols[sg].push({id:w.id||w.workerId||w.goal,goal:w.goal||w.task||w.approach||'(worker)',status:st,tokens:w.tokensUsed||w.budget,time:w.startedAt||w.createdAt,role:w.role});
|
|
589
608
|
}
|
|
590
609
|
}
|
|
610
|
+
const wfLanes = S.wfKanban?.lanes || {};
|
|
611
|
+
const wfLaneMap = {
|
|
612
|
+
backlog: 'planning',
|
|
613
|
+
ready: 'hiring',
|
|
614
|
+
claimed: 'hiring',
|
|
615
|
+
wip: 'running',
|
|
616
|
+
review: 'ghostpass',
|
|
617
|
+
blocked: 'ghostpass',
|
|
618
|
+
done: 'done',
|
|
619
|
+
failed: 'done',
|
|
620
|
+
cancelled: 'done',
|
|
621
|
+
};
|
|
622
|
+
for (const [lane, cards] of Object.entries(wfLanes)) {
|
|
623
|
+
const target = wfLaneMap[lane] || 'planning';
|
|
624
|
+
for (const card of (Array.isArray(cards) ? cards : [])) {
|
|
625
|
+
const id = card.id || card.jobId;
|
|
626
|
+
if (!id || seen.has(id)) continue;
|
|
627
|
+
seen.add(id);
|
|
628
|
+
const payload = card.payload && typeof card.payload === 'object' ? card.payload : {};
|
|
629
|
+
cols[target].push({
|
|
630
|
+
id,
|
|
631
|
+
workerId: card.workerId || payload.operativeId,
|
|
632
|
+
goal: payload.goal || card.title || '(workforce job)',
|
|
633
|
+
status: lane === 'failed' ? 'failed' : lane === 'cancelled' ? 'cancelled' : (card.status || lane),
|
|
634
|
+
tokens: card.tokensUsed || payload.tokensUsed,
|
|
635
|
+
time: card.updatedAt || card.completedAt || card.claimedAt || card.createdAt,
|
|
636
|
+
role: payload.specialistId || card.client || 'workforce',
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
591
640
|
const ghost = S.lastDecomposition?.autoGhostPass || S.lastCompletion?.autoGhostPass || op?.orchestration?.autoGhostPass || op?.autoGhostPass;
|
|
592
641
|
if (ghost && (ghost.applied || ghost.policy?.reason)) {
|
|
593
642
|
const risks = Array.isArray(ghost.riskAreas) ? ghost.riskAreas.length : 0;
|
|
@@ -606,6 +655,8 @@ function buildKanbanCols() {
|
|
|
606
655
|
for (const r of (S.runs||[]).slice(0,8)) {
|
|
607
656
|
const runId = r.runId || r.id;
|
|
608
657
|
if (!runId) continue;
|
|
658
|
+
if (seen.has(runId)) continue;
|
|
659
|
+
seen.add(runId);
|
|
609
660
|
const status = String(r.status || r.state || '').toLowerCase();
|
|
610
661
|
const stage = String(r.stage || '').toLowerCase();
|
|
611
662
|
const advisory = status === 'inspected' || /advisory|no.diff|no-diff|no mutation/i.test(String(r.result || r.summary || r.outcome || ''));
|
|
@@ -638,9 +689,10 @@ function kcardHtml(c, stage) {
|
|
|
638
689
|
const tm = c.time ? `<div class="kcard-time">${timeAgo(c.time)}</div>` : '';
|
|
639
690
|
const op = S.synapseHealth.find(o =>
|
|
640
691
|
(c.role && (o.role===c.role||o.name===c.role)) ||
|
|
692
|
+
(c.workerId && (o.id===c.workerId||o.operativeId===c.workerId)) ||
|
|
641
693
|
(c.id && (o.id===c.id||o.operativeId===c.id)));
|
|
642
694
|
const opBadge = op
|
|
643
|
-
? `<span class="agent-inline-badge st-${esc((op.status||'idle').toLowerCase())}" title="${esc(op.role||op.name||'')}">◎ ${esc((op.role||op.name||'').slice(0,18)||'agent')}</span>`
|
|
695
|
+
? `<span class="agent-inline-badge st-${esc((op.status||'idle').toLowerCase())}" title="${esc(op.displayName||op.role||op.name||'')}">◎ ${esc((op.displayName||op.role||op.name||'').slice(0,18)||'agent')}</span>`
|
|
644
696
|
: '';
|
|
645
697
|
return `<div class="kcard s-${esc(stage)}" data-runid="${esc(String(c.id))}">
|
|
646
698
|
<div class="kcard-goal">${esc(c.goal)}</div>
|
|
@@ -689,13 +741,14 @@ function renderEvents() {
|
|
|
689
741
|
function renderPulse() {
|
|
690
742
|
const el=$('system-pulse'); if (!el) return;
|
|
691
743
|
const op=S.operateSurface;
|
|
692
|
-
const
|
|
744
|
+
const activeOps=S.synapseHealth.filter(isOpActive).length;
|
|
745
|
+
const totalOps=S.synapseHealth.length;
|
|
693
746
|
const mem=op?.memorySummary?.total??op?.memory?.total??S.memories.length;
|
|
694
747
|
const gt=S.tokensSummary?.gross||0, nt=S.tokensSummary?.net||0;
|
|
695
748
|
const eff=gt>0?Math.round((1-nt/gt)*100)+'%':'—';
|
|
696
749
|
const gh=S.worktreeHealth?.pending??0;
|
|
697
750
|
const up=timeAgo(S.startTime).replace(' ago','');
|
|
698
|
-
const rows=[['Synapse',
|
|
751
|
+
const rows=[['Synapse',totalOps?`${activeOps}/${totalOps} active`:'idle'],['Memory',mem?`${fmtNum(mem)} entries`:'—'],['Efficiency',eff],['Ghost-pass',gh?`${gh} pending`:'clear'],['Uptime',up||'—']];
|
|
699
752
|
el.innerHTML=rows.map(([k,v])=>`<div class="row"><span class="row-k">${esc(k)}</span><span class="row-v">${esc(v)}</span></div>`).join('');
|
|
700
753
|
}
|
|
701
754
|
|
|
@@ -26,6 +26,10 @@ function outcomeChip(outcome) {
|
|
|
26
26
|
return `<span class="chip ${cls[outcome] || 'chip-muted'}">${esc(outcome)}</span>`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function statusRow(label, value) {
|
|
30
|
+
return `<div class="governance-status-row"><span>${esc(label)}</span><strong>${esc(value)}</strong></div>`;
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
/** Render a compact unified diff with line highlighting. */
|
|
30
34
|
function renderDiff(diff) {
|
|
31
35
|
if (!diff) return `<div class="diff-empty">No diff recorded</div>`;
|
|
@@ -65,18 +69,22 @@ export function render(cycles, health = null) {
|
|
|
65
69
|
|
|
66
70
|
if (!cycles.length) {
|
|
67
71
|
container.innerHTML = `
|
|
68
|
-
<div class="
|
|
69
|
-
<div class="card
|
|
70
|
-
<div class="
|
|
71
|
-
<div class="
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
<div class="governance-status-grid">
|
|
73
|
+
<div class="card governance-status-card">
|
|
74
|
+
<div class="governance-card-head"><span class="governance-card-title">Darwin auto-propose</span><span class="chip chip-muted">idle</span></div>
|
|
75
|
+
<div class="governance-status-body">
|
|
76
|
+
${statusRow('Proposals', '0')}
|
|
77
|
+
${statusRow('Trigger', 'nexus_orchestrate')}
|
|
78
|
+
${statusRow('Self-improve', health?.runtime?.selfImprove ? 'on' : 'off')}
|
|
75
79
|
</div>
|
|
76
80
|
</div>
|
|
77
|
-
<div class="card
|
|
78
|
-
<div class="
|
|
79
|
-
<div class="
|
|
81
|
+
<div class="card governance-status-card">
|
|
82
|
+
<div class="governance-card-head"><span class="governance-card-title">Consensus guard</span><span class="chip chip-ok">ready</span></div>
|
|
83
|
+
<div class="governance-status-copy">Live Byzantine votes and review decisions appear here when proposals are created.</div>
|
|
84
|
+
<div class="governance-status-body compact">
|
|
85
|
+
${statusRow('Review mode', 'human gated')}
|
|
86
|
+
${statusRow('Live votes', String(S.byzantineVotes?.size || 0))}
|
|
87
|
+
</div>
|
|
80
88
|
</div>
|
|
81
89
|
</div>`;
|
|
82
90
|
_renderByzantineSection();
|
|
@@ -168,7 +176,7 @@ function _fmtMetrics(before, after) {
|
|
|
168
176
|
const b = Number(before[k] ?? 0), a = Number(after[k] ?? 0);
|
|
169
177
|
const delta = a - b;
|
|
170
178
|
const cls = delta > 0 ? 'metric-up' : delta < 0 ? 'metric-down' : 'metric-flat';
|
|
171
|
-
return `<span class="darwin-metric ${cls}">${esc(k)}
|
|
179
|
+
return `<span class="darwin-metric ${cls}"><span>${esc(k)}</span><strong>${b.toFixed(2)} -> ${a.toFixed(2)}</strong><em>${delta >= 0 ? '+' : ''}${delta.toFixed(2)}</em></span>`;
|
|
172
180
|
}).join('');
|
|
173
181
|
}
|
|
174
182
|
|
|
@@ -29,6 +29,10 @@ function timeAgo(ts) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
let _activeTier = null;
|
|
32
|
+
let _graphZoom = null;
|
|
33
|
+
let _graphTransform = null;
|
|
34
|
+
let _lastGraphNodes = [];
|
|
35
|
+
let _lastGraphLinks = [];
|
|
32
36
|
|
|
33
37
|
function memText(m) {
|
|
34
38
|
return m.title || m.excerpt || m.content || m.summary || m.id || '';
|
|
@@ -309,6 +313,58 @@ function _renderGraphHud({ nodes, links, fallbackMode }) {
|
|
|
309
313
|
const files = nodes.filter(n => n.nodeType === 'file').length;
|
|
310
314
|
const mode = fallbackMode === 'none' ? 'real topology' : fallbackMode;
|
|
311
315
|
hud.innerHTML = `<span>${fmtNum(memories)} memories</span><span>${fmtNum(files)} files</span><span>${fmtNum(links.length)} links</span><span>${esc(mode)}</span>`;
|
|
316
|
+
_renderGraphControls();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function _setGraphExpanded(expanded) {
|
|
320
|
+
const c = $('graph-container');
|
|
321
|
+
const b = $('mem-graph-max-btn');
|
|
322
|
+
if (!c) return;
|
|
323
|
+
c.classList.toggle('graph-expanded', Boolean(expanded));
|
|
324
|
+
if (b) b.textContent = expanded ? 'Restore graph' : 'Maximize graph';
|
|
325
|
+
renderGraph();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function _zoomGraphBy(factor) {
|
|
329
|
+
const svg = $('graph-svg');
|
|
330
|
+
if (!svg || !_graphZoom || typeof d3 === 'undefined') return;
|
|
331
|
+
d3.select(svg).transition().duration(160).call(_graphZoom.scaleBy, factor);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function _resetGraphView() {
|
|
335
|
+
const svg = $('graph-svg');
|
|
336
|
+
if (!svg || !_graphZoom || typeof d3 === 'undefined') return;
|
|
337
|
+
d3.select(svg).transition().duration(180).call(_graphZoom.transform, d3.zoomIdentity);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function _renderGraphControls() {
|
|
341
|
+
const container = $('graph-container');
|
|
342
|
+
if (!container) return;
|
|
343
|
+
let controls = document.getElementById('memory-graph-controls');
|
|
344
|
+
if (!controls) {
|
|
345
|
+
controls = document.createElement('div');
|
|
346
|
+
controls.id = 'memory-graph-controls';
|
|
347
|
+
controls.className = 'memory-graph-controls';
|
|
348
|
+
container.appendChild(controls);
|
|
349
|
+
}
|
|
350
|
+
const expanded = container.classList.contains('graph-expanded');
|
|
351
|
+
controls.innerHTML = `
|
|
352
|
+
<button class="memory-graph-btn" data-graph-action="zoom-out" title="Zoom out" aria-label="Zoom out">-</button>
|
|
353
|
+
<button class="memory-graph-btn" data-graph-action="zoom-in" title="Zoom in" aria-label="Zoom in">+</button>
|
|
354
|
+
<button class="memory-graph-btn" data-graph-action="fit" title="Fit graph" aria-label="Fit graph">Fit</button>
|
|
355
|
+
<button class="memory-graph-btn" data-graph-action="browse" title="Browse graph nodes" aria-label="Browse graph nodes">Browse</button>
|
|
356
|
+
<button class="memory-graph-btn" data-graph-action="toggle" title="${expanded ? 'Minimize graph' : 'Maximize graph'}" aria-label="${expanded ? 'Minimize graph' : 'Maximize graph'}">${expanded ? 'Min' : 'Max'}</button>`;
|
|
357
|
+
controls.querySelectorAll('[data-graph-action]').forEach(button => {
|
|
358
|
+
button.addEventListener('click', event => {
|
|
359
|
+
event.stopPropagation();
|
|
360
|
+
const action = button.dataset.graphAction;
|
|
361
|
+
if (action === 'zoom-out') _zoomGraphBy(0.75);
|
|
362
|
+
if (action === 'zoom-in') _zoomGraphBy(1.25);
|
|
363
|
+
if (action === 'fit') _resetGraphView();
|
|
364
|
+
if (action === 'browse') _browseGraphNodes();
|
|
365
|
+
if (action === 'toggle') _setGraphExpanded(!container.classList.contains('graph-expanded'));
|
|
366
|
+
});
|
|
367
|
+
});
|
|
312
368
|
}
|
|
313
369
|
|
|
314
370
|
/* ── D3 graph ──
|
|
@@ -402,6 +458,8 @@ function renderGraph() {
|
|
|
402
458
|
const t=typeof l.target==='object'?l.target.id:l.target;
|
|
403
459
|
return nset.has(s)&&nset.has(t);
|
|
404
460
|
}).map(l=>({...l}));
|
|
461
|
+
_lastGraphNodes = nodes;
|
|
462
|
+
_lastGraphLinks = links;
|
|
405
463
|
|
|
406
464
|
if (typeof d3 === 'undefined') {
|
|
407
465
|
svg.innerHTML = '';
|
|
@@ -438,6 +496,15 @@ function renderGraph() {
|
|
|
438
496
|
.attr('cy', (hash01(`sy:${i}`) * H).toFixed(2))
|
|
439
497
|
.attr('r', 0.7 + hash01(`sr:${i}`) * 1.4);
|
|
440
498
|
}
|
|
499
|
+
const viewport=d3s.append('g').attr('class','memory-graph-viewport');
|
|
500
|
+
_graphZoom = d3.zoom()
|
|
501
|
+
.scaleExtent([0.32, 4])
|
|
502
|
+
.on('zoom', ev => {
|
|
503
|
+
_graphTransform = ev.transform;
|
|
504
|
+
viewport.attr('transform', ev.transform);
|
|
505
|
+
});
|
|
506
|
+
d3s.call(_graphZoom).on('dblclick.zoom', null);
|
|
507
|
+
if (_graphTransform) d3s.call(_graphZoom.transform, _graphTransform);
|
|
441
508
|
|
|
442
509
|
// Truthfulness: if the backend couldn't produce a real graph, clear the SVG
|
|
443
510
|
// and show the explicit empty-state banner. No synthetic edges rendered.
|
|
@@ -490,24 +557,25 @@ function renderGraph() {
|
|
|
490
557
|
.force('collision',d3.forceCollide(d=>d.nodeType==='file'?8:10+d.priority*8));
|
|
491
558
|
S.graphSim=sim;
|
|
492
559
|
|
|
493
|
-
const linkEl=
|
|
560
|
+
const linkEl=viewport.append('g').attr('class','memory-links').selectAll('line').data(links).enter().append('line')
|
|
494
561
|
.attr('stroke',d=>d.type==='tag'?'#52525b':'#27272a')
|
|
495
562
|
.attr('stroke-width',d=>d.type==='tag'?1.2:0.8)
|
|
496
563
|
.attr('stroke-opacity',d=>d.type==='tag'?0.45:0.22);
|
|
497
564
|
const fileNodes=nodes.filter(n=>n.nodeType==='file');
|
|
498
565
|
const memNodes =nodes.filter(n=>n.nodeType!=='file');
|
|
499
|
-
const fileEl=
|
|
566
|
+
const fileEl=viewport.append('g').attr('class','memory-file-nodes').selectAll('rect').data(fileNodes).enter().append('rect')
|
|
500
567
|
.attr('width',d=>5+d.priority*6).attr('height',d=>5+d.priority*6)
|
|
501
568
|
.attr('rx',1.5).attr('fill','#6366f1').attr('fill-opacity',0.58).attr('stroke','#000').attr('stroke-width',1)
|
|
502
569
|
.style('cursor','pointer')
|
|
570
|
+
.on('click',(_,d)=>_openGraphFileDrawer(d))
|
|
503
571
|
.on('mouseover',function(ev,d){ _memHover(d, nodeEl, fileEl, linkEl); _showTip(ev,d.label); })
|
|
504
572
|
.on('mouseout', function(){ _memHoverOut(nodeEl, fileEl, linkEl); _hideTip(); });
|
|
505
573
|
const nc=d=>{ const age=Date.now()-(d.createdAt||Date.now()); if(age<3600000) return '#00d4ff'; if(age<86400000) return '#00ff88'; if(age<604800000) return '#ffd14d'; return '#ff5f57'; };
|
|
506
574
|
// Mark promoted nodes (hippocampus or cortex) for pulsating ring
|
|
507
575
|
const isPromoted=d=>(d.tier==='hippocampus'||d.tier==='cortex'||d.tier==='episodic'||d.tier==='semantic');
|
|
508
|
-
const haloEl=
|
|
576
|
+
const haloEl=viewport.append('g').attr('class','memory-node-halos').selectAll('circle').data(memNodes).enter().append('circle')
|
|
509
577
|
.attr('r',d=>10+d.priority*16).attr('fill','url(#memory-node-glow)').attr('opacity',0.18);
|
|
510
|
-
const nodeEl=
|
|
578
|
+
const nodeEl=viewport.append('g').attr('class','memory-nodes').selectAll('circle').data(memNodes).enter().append('circle')
|
|
511
579
|
.attr('r',d=>4+d.priority*9).attr('fill',nc).attr('fill-opacity',0.85)
|
|
512
580
|
.attr('stroke',d=>decayStroke(d.decayState)).attr('stroke-width',d=>isPromoted(d)||d.decayState==='stale'||d.decayState==='retiring'?1.8:1)
|
|
513
581
|
.style('cursor','pointer')
|
|
@@ -647,6 +715,99 @@ function _browseMemories() {
|
|
|
647
715
|
});
|
|
648
716
|
}
|
|
649
717
|
|
|
718
|
+
function _linkedNodeIds(id) {
|
|
719
|
+
const linked = new Set();
|
|
720
|
+
for (const link of _lastGraphLinks) {
|
|
721
|
+
const source = typeof link.source === 'object' ? link.source.id : link.source;
|
|
722
|
+
const target = typeof link.target === 'object' ? link.target.id : link.target;
|
|
723
|
+
if (source === id) linked.add(target);
|
|
724
|
+
if (target === id) linked.add(source);
|
|
725
|
+
}
|
|
726
|
+
return linked;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function _browseGraphNodes() {
|
|
730
|
+
const nodes = (_lastGraphNodes.length ? _lastGraphNodes : S.gNodes).slice(0, 180);
|
|
731
|
+
const files = nodes.filter(n => n.nodeType === 'file').length;
|
|
732
|
+
const memories = nodes.length - files;
|
|
733
|
+
openDrawer({
|
|
734
|
+
title: `Graph nodes (${nodes.length})`,
|
|
735
|
+
body: nodes.length ? `<div class="memory-graph-node-browser">
|
|
736
|
+
<div class="memory-graph-node-summary">
|
|
737
|
+
<span class="chip chip-muted">${fmtNum(memories)} memories</span>
|
|
738
|
+
<span class="chip chip-muted">${fmtNum(files)} files</span>
|
|
739
|
+
<span class="chip chip-muted">${fmtNum(_lastGraphLinks.length)} links</span>
|
|
740
|
+
</div>
|
|
741
|
+
${nodes.map(n => {
|
|
742
|
+
const linked = _linkedNodeIds(n.id).size;
|
|
743
|
+
const type = n.nodeType === 'file' ? 'file' : (n.tier || 'memory');
|
|
744
|
+
return `<button class="memory-graph-node-row" data-graph-node="${esc(n.id)}" data-graph-node-type="${esc(n.nodeType || 'memory')}">
|
|
745
|
+
<span class="memory-graph-node-type">${esc(type)}</span>
|
|
746
|
+
<span class="memory-graph-node-title">${esc(n.label || n.id)}</span>
|
|
747
|
+
<span class="memory-graph-node-links">${fmtNum(linked)} links</span>
|
|
748
|
+
</button>`;
|
|
749
|
+
}).join('')}
|
|
750
|
+
</div>` : '<div class="empty-sub">No graph nodes are visible for the current filter.</div>',
|
|
751
|
+
});
|
|
752
|
+
document.querySelectorAll('[data-graph-node]').forEach(row => {
|
|
753
|
+
row.addEventListener('click', () => {
|
|
754
|
+
const node = nodes.find(item => item.id === row.dataset.graphNode);
|
|
755
|
+
if (!node) return;
|
|
756
|
+
if (node.nodeType === 'file') _openGraphFileDrawer(node);
|
|
757
|
+
else _openMemDrawer(node);
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function _openGraphFileDrawer(node) {
|
|
763
|
+
const linkedIds = _linkedNodeIds(node.id);
|
|
764
|
+
const linkedMemories = S.gNodes
|
|
765
|
+
.filter(item => linkedIds.has(item.id) && item.nodeType !== 'file')
|
|
766
|
+
.slice(0, 12);
|
|
767
|
+
const filePath = node.data?.path || node.label || node.id;
|
|
768
|
+
openDrawer({
|
|
769
|
+
title: filePath.split('/').pop() || 'File node',
|
|
770
|
+
body: `<div class="dsec"><div class="dsec-title">File node</div>
|
|
771
|
+
<div class="drow"><span class="drow-k">Path</span><span class="drow-v">${esc(filePath)}</span></div>
|
|
772
|
+
<div class="drow"><span class="drow-k">Links</span><span class="drow-v">${fmtNum(linkedIds.size)}</span></div>
|
|
773
|
+
<div class="drow"><span class="drow-k">Graph source</span><span class="drow-v">${esc(S.topology?.provenance?.graphSource || S.topology?.meta?.graphSource || 'topology')}</span></div>
|
|
774
|
+
</div>
|
|
775
|
+
<div class="dsec memory-drawer-actions">
|
|
776
|
+
<button class="btn btn-sm" id="mem-file-filter-btn">Filter memories</button>
|
|
777
|
+
<button class="btn btn-sm" id="mem-file-repo-btn">Open repo graph</button>
|
|
778
|
+
</div>
|
|
779
|
+
<div class="dsec"><div class="dsec-title">Linked memories</div>
|
|
780
|
+
${linkedMemories.length ? linkedMemories.map(m => `<button class="memory-graph-node-row" data-linked-memory="${esc(m.id)}">
|
|
781
|
+
<span class="memory-graph-node-type">${esc(m.tier || 'memory')}</span>
|
|
782
|
+
<span class="memory-graph-node-title">${esc(m.label || m.id)}</span>
|
|
783
|
+
</button>`).join('') : '<div class="empty-sub">No memory node is linked to this file in the visible graph.</div>'}
|
|
784
|
+
</div>`,
|
|
785
|
+
});
|
|
786
|
+
$('mem-file-filter-btn')?.addEventListener('click', () => {
|
|
787
|
+
const input = $('mem-search');
|
|
788
|
+
S.memQuery = filePath.split('/').pop() || filePath;
|
|
789
|
+
if (input) input.value = S.memQuery;
|
|
790
|
+
renderMemList();
|
|
791
|
+
renderGraph();
|
|
792
|
+
});
|
|
793
|
+
$('mem-file-repo-btn')?.addEventListener('click', () => {
|
|
794
|
+
window.location.hash = '#repo';
|
|
795
|
+
setTimeout(() => {
|
|
796
|
+
const input = $('repo-search-input');
|
|
797
|
+
if (input) {
|
|
798
|
+
input.value = filePath.split('/').pop() || filePath;
|
|
799
|
+
input.dispatchEvent(new Event('input'));
|
|
800
|
+
}
|
|
801
|
+
}, 120);
|
|
802
|
+
});
|
|
803
|
+
document.querySelectorAll('[data-linked-memory]').forEach(row => {
|
|
804
|
+
row.addEventListener('click', () => {
|
|
805
|
+
const memory = S.gNodes.find(item => item.id === row.dataset.linkedMemory);
|
|
806
|
+
if (memory) _openMemDrawer(memory);
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
650
811
|
function _openMemDrawer(node) {
|
|
651
812
|
const m=node.data||node;
|
|
652
813
|
const tags=(m.tags||[]).map(t=>`<span class="chip">${esc(t)}</span>`).join(' ');
|
|
@@ -698,11 +859,8 @@ export function init() {
|
|
|
698
859
|
$('mem-list-browse-btn')?.addEventListener('click', _browseMemories);
|
|
699
860
|
$('mem-graph-max-btn')?.addEventListener('click', () => {
|
|
700
861
|
const c = $('graph-container');
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const expanded = c.classList.toggle('graph-expanded');
|
|
704
|
-
b.textContent = expanded ? 'Restore graph' : 'Maximize graph';
|
|
705
|
-
renderGraph();
|
|
862
|
+
if (!c) return;
|
|
863
|
+
_setGraphExpanded(!c.classList.contains('graph-expanded'));
|
|
706
864
|
});
|
|
707
865
|
$('mem-graph-focus-btn')?.addEventListener('click', () => {
|
|
708
866
|
const selected = selectedMemories();
|
|
@@ -34,6 +34,8 @@ let _tokenFlyoutOpen = false;
|
|
|
34
34
|
let _tokenFlyoutLoading = false;
|
|
35
35
|
let _tokenFlyoutError = '';
|
|
36
36
|
let _tokenTelemetry = null;
|
|
37
|
+
let _runtimeSnapshot = null;
|
|
38
|
+
let _runtimeSnapshotLoading = false;
|
|
37
39
|
|
|
38
40
|
/* ── Category metadata ──────────────────────────────────────────────────────── */
|
|
39
41
|
const CATEGORY_META = {
|
|
@@ -84,6 +86,28 @@ function fmtPct(n) {
|
|
|
84
86
|
return `${Math.round(v)}%`;
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
function countKanbanJobs(board) {
|
|
90
|
+
const lanes = board?.lanes ?? {};
|
|
91
|
+
let active = 0;
|
|
92
|
+
let terminal = 0;
|
|
93
|
+
let total = Number(board?.totalJobs ?? 0);
|
|
94
|
+
for (const [lane, cards] of Object.entries(lanes)) {
|
|
95
|
+
const count = Array.isArray(cards) ? cards.length : 0;
|
|
96
|
+
if (!total) total += count;
|
|
97
|
+
if (['done','failed','cancelled'].includes(lane)) terminal += count;
|
|
98
|
+
else active += count;
|
|
99
|
+
}
|
|
100
|
+
return { active, terminal, total };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeOps(raw) {
|
|
104
|
+
const ops = Array.isArray(raw) ? raw : (Array.isArray(raw?.operatives) ? raw.operatives : []);
|
|
105
|
+
return ops.map(op => {
|
|
106
|
+
const status = String(op?.state ?? op?.healthState ?? op?.status ?? 'idle').toLowerCase();
|
|
107
|
+
return { ...op, status };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
87
111
|
function fmtTime(ts) {
|
|
88
112
|
const n = Number(ts ?? 0);
|
|
89
113
|
if (!Number.isFinite(n) || n <= 0) return 'recent';
|
|
@@ -190,7 +214,19 @@ function mount() {
|
|
|
190
214
|
</button>
|
|
191
215
|
<div class="rt-kpi">
|
|
192
216
|
<div class="rt-kpi-val" id="rt-active-count">0</div>
|
|
193
|
-
<div class="rt-kpi-lbl">Active
|
|
217
|
+
<div class="rt-kpi-lbl">Active Tools</div>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="rt-kpi">
|
|
220
|
+
<div class="rt-kpi-val" id="rt-operative-count">—</div>
|
|
221
|
+
<div class="rt-kpi-lbl">Operatives</div>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="rt-kpi">
|
|
224
|
+
<div class="rt-kpi-val" id="rt-job-count">—</div>
|
|
225
|
+
<div class="rt-kpi-lbl">Jobs</div>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="rt-kpi">
|
|
228
|
+
<div class="rt-kpi-val" id="rt-client-count">—</div>
|
|
229
|
+
<div class="rt-kpi-lbl">Clients</div>
|
|
194
230
|
</div>
|
|
195
231
|
</div>
|
|
196
232
|
|
|
@@ -263,15 +299,68 @@ function renderKPIs() {
|
|
|
263
299
|
const toolEl = $('rt-toolcalls');
|
|
264
300
|
const savedEl = $('rt-tokens-saved');
|
|
265
301
|
const activeEl = $('rt-active-count');
|
|
302
|
+
const operativeEl = $('rt-operative-count');
|
|
303
|
+
const jobEl = $('rt-job-count');
|
|
304
|
+
const clientEl = $('rt-client-count');
|
|
305
|
+
const lifetimeSaved = Number(
|
|
306
|
+
_runtimeSnapshot?.tokens?.savedTokens
|
|
307
|
+
?? _runtimeSnapshot?.tokens?.totalSaved
|
|
308
|
+
?? _runtimeSnapshot?.summary?.savedTokens
|
|
309
|
+
?? _runtimeSnapshot?.summary?.saved
|
|
310
|
+
?? 0
|
|
311
|
+
);
|
|
312
|
+
const displaySaved = _totalTokensSaved > 0 ? _totalTokensSaved : lifetimeSaved;
|
|
313
|
+
const ops = normalizeOps(_runtimeSnapshot?.operatives);
|
|
314
|
+
const activeOps = ops.filter(op => ['active','running','wip','claimed'].includes(op.status)).length;
|
|
315
|
+
const jobs = countKanbanJobs(_runtimeSnapshot?.kanban);
|
|
316
|
+
const clients = _runtimeSnapshot?.health?.runtimeEnvelope?.clients ?? _runtimeSnapshot?.health?.clients ?? {};
|
|
317
|
+
const activeClients = Number(clients.active ?? clients.activeCount ?? 0);
|
|
318
|
+
const totalClients = Number(clients.total ?? clients.totalCount ?? 0);
|
|
266
319
|
if (toolEl) toolEl.textContent = _toolCalls.toLocaleString();
|
|
267
|
-
if (savedEl) savedEl.textContent =
|
|
268
|
-
? (
|
|
320
|
+
if (savedEl) savedEl.textContent = displaySaved > 0
|
|
321
|
+
? (displaySaved >= 1000 ? `${(displaySaved / 1000).toFixed(1)}k` : String(displaySaved))
|
|
269
322
|
: '0';
|
|
270
323
|
if (activeEl) activeEl.textContent = String(_activeTools.size);
|
|
324
|
+
if (operativeEl) operativeEl.textContent = ops.length ? `${activeOps}/${ops.length}` : '0';
|
|
325
|
+
if (jobEl) jobEl.textContent = jobs.total ? `${jobs.active}/${jobs.total}` : '0';
|
|
326
|
+
if (clientEl) clientEl.textContent = totalClients ? `${activeClients}/${totalClients}` : '0';
|
|
271
327
|
const tokenBtn = $('rt-kpi-saved');
|
|
272
328
|
if (tokenBtn) tokenBtn.setAttribute('aria-expanded', _tokenFlyoutOpen ? 'true' : 'false');
|
|
273
329
|
}
|
|
274
330
|
|
|
331
|
+
async function refreshRuntimeSnapshot() {
|
|
332
|
+
if (_runtimeSnapshotLoading) return;
|
|
333
|
+
_runtimeSnapshotLoading = true;
|
|
334
|
+
try {
|
|
335
|
+
const [summary, lifetimeRaw, health, kanban, operatives] = await Promise.allSettled([
|
|
336
|
+
api('/api/tokens/summary', 0),
|
|
337
|
+
api('/api/tokens/lifetime', 0),
|
|
338
|
+
api('/api/health', 15_000),
|
|
339
|
+
api('/api/workforce/kanban', 0),
|
|
340
|
+
api('/api/synapse/health', 0),
|
|
341
|
+
]);
|
|
342
|
+
_runtimeSnapshot = {
|
|
343
|
+
summary: summary.status === 'fulfilled' ? summary.value : null,
|
|
344
|
+
tokens: lifetimeRaw.status === 'fulfilled' ? (lifetimeRaw.value?.data ?? lifetimeRaw.value) : null,
|
|
345
|
+
health: health.status === 'fulfilled' ? health.value : null,
|
|
346
|
+
kanban: kanban.status === 'fulfilled' ? kanban.value : null,
|
|
347
|
+
operatives: operatives.status === 'fulfilled' ? operatives.value : null,
|
|
348
|
+
};
|
|
349
|
+
if (!_tokenTelemetry) {
|
|
350
|
+
_tokenTelemetry = {
|
|
351
|
+
summary: _runtimeSnapshot.summary ?? {},
|
|
352
|
+
lifetime: _runtimeSnapshot.tokens ?? {},
|
|
353
|
+
bySource: {},
|
|
354
|
+
timeline: [],
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
} finally {
|
|
358
|
+
_runtimeSnapshotLoading = false;
|
|
359
|
+
renderKPIs();
|
|
360
|
+
renderTokenFlyout();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
275
364
|
async function loadTokenTelemetry() {
|
|
276
365
|
_tokenFlyoutLoading = true;
|
|
277
366
|
_tokenFlyoutError = '';
|
|
@@ -637,6 +726,7 @@ export function ingestEvent(evt) {
|
|
|
637
726
|
export function load() {
|
|
638
727
|
if (!_mounted) mount();
|
|
639
728
|
else renderAll();
|
|
729
|
+
void refreshRuntimeSnapshot();
|
|
640
730
|
}
|
|
641
731
|
|
|
642
732
|
export function render() {
|
|
@@ -46,6 +46,18 @@ function lifecycleLabel(card, payload) {
|
|
|
46
46
|
return 'Queued';
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function normalizeStatus(value) {
|
|
50
|
+
return String(value || 'idle').toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isActiveStatus(value) {
|
|
54
|
+
return ['active','running','wip','claimed'].includes(normalizeStatus(value));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function opLabel(op) {
|
|
58
|
+
return firstText(op?.displayName, op?.name, op?.role, op?.roleTitle, op?.specialistId, op?.id, 'agent');
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
/* ── Active-dispatch state (push-mode) ── */
|
|
50
62
|
// Keyed by runId. Entries are created on dispatch.started and removed on complete/failed/cancelled.
|
|
51
63
|
const _dispatches = new Map();
|
|
@@ -156,7 +168,7 @@ function _buildDispatchStrip(run) {
|
|
|
156
168
|
|
|
157
169
|
/* ── Data loader ── */
|
|
158
170
|
export async function load() {
|
|
159
|
-
const [teams, health, disp, appr, assets, healthData, kanban, sortiesRes] = await Promise.all([
|
|
171
|
+
const [teams, health, disp, appr, assets, healthData, kanban, sortiesRes, curated] = await Promise.all([
|
|
160
172
|
api('/api/synapse/teams', 5000),
|
|
161
173
|
api('/api/synapse/health', 5000),
|
|
162
174
|
api('/api/architects/dispatch', 5000),
|
|
@@ -165,6 +177,7 @@ export async function load() {
|
|
|
165
177
|
api('/api/health', 15000),
|
|
166
178
|
api('/api/workforce/kanban', 5000),
|
|
167
179
|
api('/api/synapse/sorties?limit=40', 5000),
|
|
180
|
+
api('/api/specialists/curated', 60000),
|
|
168
181
|
]);
|
|
169
182
|
S.synapseTeams = teams;
|
|
170
183
|
S.synapseHealthRaw = health;
|
|
@@ -175,14 +188,17 @@ export async function load() {
|
|
|
175
188
|
S.healthData = healthData;
|
|
176
189
|
S.wfKanban = kanban;
|
|
177
190
|
S.sorties = Array.isArray(sortiesRes?.sorties) ? sortiesRes.sorties : [];
|
|
191
|
+
S.curatedSpecialists = Array.isArray(curated) ? curated : S.curatedSpecialists;
|
|
178
192
|
notifyNotReady([teams, health, disp]);
|
|
179
193
|
render();
|
|
180
194
|
}
|
|
181
195
|
|
|
182
196
|
function _norm(op) {
|
|
183
197
|
if (!op||typeof op!=='object') return op;
|
|
184
|
-
|
|
185
|
-
|
|
198
|
+
const displayName = op.name || op.displayName || op.role || op.roleTitle || op.specialistId || op.id || op.operativeId;
|
|
199
|
+
return { ...op, displayName,
|
|
200
|
+
role: op.role||op.roleTitle||op.specialistId||displayName||null,
|
|
201
|
+
status: normalizeStatus(op.state||op.healthState||op.status||'IDLE'),
|
|
186
202
|
budget: op.budgetCapUsd??op.budget??null,
|
|
187
203
|
budgetUsed: op.spentUsd??op.budgetUsed??0,
|
|
188
204
|
team: op.strikeTeamId||op.team||op.strikeName||null };
|
|
@@ -226,7 +242,17 @@ function renderKanban() {
|
|
|
226
242
|
return;
|
|
227
243
|
}
|
|
228
244
|
|
|
229
|
-
const
|
|
245
|
+
const laneOrder = [...KANBAN_LANES].sort((a, b) => {
|
|
246
|
+
const ac = (lanes[a] ?? []).length;
|
|
247
|
+
const bc = (lanes[b] ?? []).length;
|
|
248
|
+
if (Boolean(bc) !== Boolean(ac)) return Boolean(bc) - Boolean(ac);
|
|
249
|
+
return KANBAN_LANES.indexOf(a) - KANBAN_LANES.indexOf(b);
|
|
250
|
+
});
|
|
251
|
+
const laneSummary = laneOrder
|
|
252
|
+
.filter(lane => (lanes[lane] ?? []).length)
|
|
253
|
+
.map(lane => `<span class="kanban-summary-chip">${esc(LANE_LABEL[lane] ?? lane)} ${fmtNum((lanes[lane] ?? []).length)}</span>`)
|
|
254
|
+
.join('');
|
|
255
|
+
const lanesHtml = laneOrder.map(lane => {
|
|
230
256
|
const cards = lanes[lane] ?? [];
|
|
231
257
|
if (cards.length === 0 && ['cancelled','failed'].includes(lane)) return '';
|
|
232
258
|
const cls = LANE_CLASS[lane] ?? '';
|
|
@@ -246,8 +272,11 @@ function renderKanban() {
|
|
|
246
272
|
</div>`;
|
|
247
273
|
}).filter(Boolean).join('');
|
|
248
274
|
|
|
249
|
-
el.innerHTML = `<div class="kanban-meta">Total: ${total} jobs</div>
|
|
275
|
+
el.innerHTML = `<div class="kanban-meta"><span>Total: ${total} jobs</span>${laneSummary ? `<span class="kanban-summary">${laneSummary}</span>` : ''}</div>
|
|
250
276
|
<div class="kanban-scroll">${lanesHtml || '<div class="kanban-empty">No visible lanes for the selected filters.</div>'}</div>`;
|
|
277
|
+
el.querySelectorAll('[data-jobid]').forEach(card => {
|
|
278
|
+
card.addEventListener('click', () => _openKanbanJobDrawer(card.dataset.jobid));
|
|
279
|
+
});
|
|
251
280
|
}
|
|
252
281
|
|
|
253
282
|
function _buildKanbanCard(c, lane) {
|
|
@@ -266,7 +295,7 @@ function _buildKanbanCard(c, lane) {
|
|
|
266
295
|
runId ? `<span>${esc(String(runId).slice(0, 12))}</span>` : '',
|
|
267
296
|
c.tokensUsed ? `<span>${fmtNum(c.tokensUsed)}t</span>` : '',
|
|
268
297
|
].filter(Boolean).join('');
|
|
269
|
-
return `<div class="kanban-card" title="${esc([title, goal, actual].filter(Boolean).join(' · '))}">
|
|
298
|
+
return `<div class="kanban-card" data-jobid="${esc(c.id)}" title="${esc([title, goal, actual].filter(Boolean).join(' · '))}">
|
|
270
299
|
<div class="kc-top">${pri}<span class="kc-id">${shortId}</span></div>
|
|
271
300
|
<div class="kc-title">${title}</div>
|
|
272
301
|
${goal ? `<div class="kc-goal">${esc(goal)}</div>` : ''}
|
|
@@ -279,6 +308,43 @@ function _buildKanbanCard(c, lane) {
|
|
|
279
308
|
</div>`;
|
|
280
309
|
}
|
|
281
310
|
|
|
311
|
+
function _findKanbanJob(jobId) {
|
|
312
|
+
const lanes = S.wfKanban?.lanes ?? {};
|
|
313
|
+
for (const [lane, cards] of Object.entries(lanes)) {
|
|
314
|
+
const found = (Array.isArray(cards) ? cards : []).find(card => String(card.id) === String(jobId));
|
|
315
|
+
if (found) return { lane, card: found };
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function _openKanbanJobDrawer(jobId) {
|
|
321
|
+
const hit = _findKanbanJob(jobId);
|
|
322
|
+
if (!hit) return;
|
|
323
|
+
const { lane, card } = hit;
|
|
324
|
+
const payload = card.payload && typeof card.payload === 'object' ? card.payload : {};
|
|
325
|
+
const drows=rows=>rows.map(([k,v])=>`<div class="drow"><span class="drow-k">${esc(k)}</span><span class="drow-v">${esc(String(v??'—'))}</span></div>`).join('');
|
|
326
|
+
const lifecycle = Array.isArray(payload.lifecycle) ? payload.lifecycle : [];
|
|
327
|
+
const files = Array.isArray(payload.filesChanged) ? payload.filesChanged : [];
|
|
328
|
+
openDrawer({
|
|
329
|
+
title: card.title || `Job ${String(card.id).slice(0, 8)}`,
|
|
330
|
+
body: `<div class="dsec"><div class="dsec-title">Workforce job</div>${drows([
|
|
331
|
+
['ID', card.id],
|
|
332
|
+
['Lane', LANE_LABEL[lane] ?? lane],
|
|
333
|
+
['Worker', card.workerId || payload.operativeId || '—'],
|
|
334
|
+
['Mode', payload.mode || card.client || '—'],
|
|
335
|
+
['Run', payload.runId || payload.completedRunId || '—'],
|
|
336
|
+
['Tokens', card.tokensUsed || payload.tokensUsed || 0],
|
|
337
|
+
])}</div>
|
|
338
|
+
<div class="dsec"><div class="dsec-title">Goal</div><div class="dcontent">${esc(payload.goal || card.title || '—')}</div></div>
|
|
339
|
+
<div class="dsec"><div class="dsec-title">Expected vs actual</div>${drows([
|
|
340
|
+
['Expected', payload.expectedBehavior || 'Worker owns this dashboard job and closes it with proof.'],
|
|
341
|
+
['Actual', payload.actualStatus || lifecycleLabel({ status: lane }, payload)],
|
|
342
|
+
])}</div>
|
|
343
|
+
${lifecycle.length ? `<div class="dsec"><div class="dsec-title">Lifecycle</div><div class="dtags">${lifecycle.map(step => `<span class="chip chip-muted">${esc(step)}</span>`).join('')}</div></div>` : ''}
|
|
344
|
+
${files.length ? `<div class="dsec"><div class="dsec-title">Files changed</div><div class="dtags">${files.slice(0, 16).map(file => `<span class="chip">${esc(file)}</span>`).join('')}</div></div>` : ''}`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
282
348
|
/* ── Sortie timeline (kanban diff + parallel execution view) ── */
|
|
283
349
|
function renderSortieTimeline() {
|
|
284
350
|
const el = document.getElementById('sortie-timeline');
|
|
@@ -336,11 +402,15 @@ function renderSortieTimeline() {
|
|
|
336
402
|
|
|
337
403
|
/* ── Org chart ── */
|
|
338
404
|
function _renderOpNode(op) {
|
|
339
|
-
const
|
|
405
|
+
const st = normalizeStatus(op.status);
|
|
406
|
+
const sc=isActiveStatus(st)?'active':st==='zombie'||st==='dead'||st==='failed'?'dead':'';
|
|
340
407
|
const interval=op.sortieIntervalMs||60000, lastAt=op.lastSortieAt||op.heartbeatAt||Date.now();
|
|
341
408
|
const pillId=`op-pill-${esc(op.id||op.operativeId||'')}`;
|
|
342
|
-
|
|
343
|
-
|
|
409
|
+
const label = opLabel(op);
|
|
410
|
+
const sub = firstText(st, op.specialistId, op.team);
|
|
411
|
+
return `<div class="org-box" id="${pillId}" data-opid="${esc(op.id||op.operativeId)}" data-opname="${esc(label)}" data-interval="${interval}" data-lastat="${lastAt}">
|
|
412
|
+
<span class="op-status-dot ${esc(sc)}"></span>
|
|
413
|
+
<span class="org-op-text"><span class="org-op-name">${esc(label)}</span><span class="org-op-meta">${esc(sub)}</span></span>
|
|
344
414
|
</div>`;
|
|
345
415
|
}
|
|
346
416
|
|
|
@@ -403,9 +473,39 @@ function renderOrgChart() {
|
|
|
403
473
|
}
|
|
404
474
|
|
|
405
475
|
/* ── Missions ── */
|
|
476
|
+
function _jobsFromKanban(limit = 20) {
|
|
477
|
+
const lanes = S.wfKanban?.lanes ?? {};
|
|
478
|
+
return Object.entries(lanes)
|
|
479
|
+
.flatMap(([lane, cards]) => (Array.isArray(cards) ? cards : []).map(card => ({ lane, card })))
|
|
480
|
+
.sort((a, b) => Number(b.card.updatedAt || b.card.claimedAt || b.card.createdAt || 0) - Number(a.card.updatedAt || a.card.claimedAt || a.card.createdAt || 0))
|
|
481
|
+
.slice(0, limit);
|
|
482
|
+
}
|
|
483
|
+
|
|
406
484
|
function renderMissions() {
|
|
407
485
|
const el=$('mission-list'); if (!el) return;
|
|
408
|
-
if (!S.missions.length) {
|
|
486
|
+
if (!S.missions.length) {
|
|
487
|
+
const jobs = _jobsFromKanban(8);
|
|
488
|
+
if (!jobs.length) {
|
|
489
|
+
el.innerHTML=`<div class="empty"><div class="empty-title">No active missions</div><div class="empty-sub">Missions appear after a goal dispatches through Synapse.</div></div>`;
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
el.innerHTML=jobs.map(({ lane, card }) => {
|
|
493
|
+
const payload = card.payload && typeof card.payload === 'object' ? card.payload : {};
|
|
494
|
+
const icon=lane==='done'?'✓':lane==='failed'?'!':lane==='wip'||lane==='claimed'?'●':'○';
|
|
495
|
+
return `<div class="mission-row" data-jobid="${esc(card.id)}">
|
|
496
|
+
<span style="color:${lane==='failed'?'var(--warning)':'var(--accent)'}">${icon}</span>
|
|
497
|
+
<div class="mission-body">
|
|
498
|
+
<div class="mission-title">${esc(payload.goal || card.title || 'Workforce job')}</div>
|
|
499
|
+
<div class="mission-meta">${statusChip(lane)} ${esc(card.workerId||payload.operativeId||'unassigned')}</div>
|
|
500
|
+
</div>
|
|
501
|
+
<span class="mission-budget">${esc(payload.mode || card.client || '')}</span>
|
|
502
|
+
</div>`;
|
|
503
|
+
}).join('');
|
|
504
|
+
el.querySelectorAll('[data-jobid]').forEach(row => {
|
|
505
|
+
row.addEventListener('click', () => _openKanbanJobDrawer(row.dataset.jobid));
|
|
506
|
+
});
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
409
509
|
el.innerHTML=S.missions.slice(0,20).map(m=>{
|
|
410
510
|
const icon=m.status==='complete'||m.status==='done'?'✓':m.status==='active'||m.status==='running'?'●':'○';
|
|
411
511
|
const bud=m.budgetUsed!=null&&m.budget!=null?`${fmtNum(m.budgetUsed)}/${fmtNum(m.budget)}t`:'';
|
|
@@ -427,7 +527,35 @@ function renderMissions() {
|
|
|
427
527
|
function renderDispatch() {
|
|
428
528
|
const el=$('dispatch-panel'); if (!el) return;
|
|
429
529
|
const d=S.archDispatch;
|
|
430
|
-
if (!d||!d.activeWorklists?.length) {
|
|
530
|
+
if (!d||!d.activeWorklists?.length) {
|
|
531
|
+
const dispatchRuns = [..._dispatches.values()].slice(0, 8);
|
|
532
|
+
const jobs = _jobsFromKanban(6);
|
|
533
|
+
if (!dispatchRuns.length && !jobs.length) {
|
|
534
|
+
el.innerHTML=`<div class="empty"><div class="empty-title">No active dispatch</div><div class="empty-sub">Architects worklists appear when a mission attaches real work items.</div></div>`;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const runRows = dispatchRuns.map(run => `<div class="mission-row">
|
|
538
|
+
<div class="mission-body">
|
|
539
|
+
<div class="mission-title">${esc(run.runId)}</div>
|
|
540
|
+
<div class="mission-meta">${statusChip(run.status)} ${esc(run.operativeId || '')}</div>
|
|
541
|
+
</div>
|
|
542
|
+
<span class="mission-budget">${run.tokens ? `${fmtNum(run.tokens)}t` : ''}</span>
|
|
543
|
+
</div>`);
|
|
544
|
+
const jobRows = jobs.map(({ lane, card }) => {
|
|
545
|
+
const payload = card.payload && typeof card.payload === 'object' ? card.payload : {};
|
|
546
|
+
return `<div class="mission-row" data-jobid="${esc(card.id)}">
|
|
547
|
+
<div class="mission-body">
|
|
548
|
+
<div class="mission-title">${esc(payload.runId || payload.completedRunId || card.title || card.id)}</div>
|
|
549
|
+
<div class="mission-meta">${statusChip(lane)} ${esc(payload.actualStatus || lifecycleLabel({ status: lane }, payload))}</div>
|
|
550
|
+
</div>
|
|
551
|
+
</div>`;
|
|
552
|
+
});
|
|
553
|
+
el.innerHTML = [...runRows, ...jobRows].join('');
|
|
554
|
+
el.querySelectorAll('[data-jobid]').forEach(row => {
|
|
555
|
+
row.addEventListener('click', () => _openKanbanJobDrawer(row.dataset.jobid));
|
|
556
|
+
});
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
431
559
|
el.innerHTML=d.activeWorklists.slice(0,10).map(wl=>{
|
|
432
560
|
return `<div class="mission-row">
|
|
433
561
|
<div class="mission-body">
|
|
@@ -442,12 +570,22 @@ function renderDispatch() {
|
|
|
442
570
|
function renderSpecialistGrid() {
|
|
443
571
|
const el=$('specialist-grid'); if (!el) return;
|
|
444
572
|
const surface=S.assetsSurface;
|
|
445
|
-
const
|
|
446
|
-
|
|
573
|
+
const rawSpecs=(surface?.specialists?.length ? surface.specialists : S.curatedSpecialists || []).slice(0,20);
|
|
574
|
+
const specs=rawSpecs.map(s => ({
|
|
575
|
+
id: s.id || s.specialistId || s.slug,
|
|
576
|
+
name: s.name || s.title || s.id || s.specialistId,
|
|
577
|
+
domains: s.domains || s.tags || (s.description ? [s.description] : []),
|
|
578
|
+
pricing: s.pricing,
|
|
579
|
+
})).filter(s => s.id);
|
|
580
|
+
if (!specs.length) {
|
|
581
|
+
el.innerHTML=`<div class="empty"><div class="empty-title">No specialists available</div><div class="empty-sub">Catalog data did not arrive yet. Hired operatives and jobs still appear above.</div></div>`;
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
447
584
|
el.innerHTML=specs.map(s=>{
|
|
448
585
|
return `<div class="spec-card" data-specid="${esc(s.id)}" data-specname="${esc(s.name||s.id)}">
|
|
449
586
|
<div class="spec-name">${esc(s.name||s.id)}</div>
|
|
450
587
|
<div class="spec-domain">${esc((s.domains||[]).slice(0,3).join(', ')||'—')}</div>
|
|
588
|
+
${s.pricing?.typical != null ? `<div class="spec-cost">~$${esc(String(s.pricing.typical))}/sortie</div>` : ''}
|
|
451
589
|
<button class="btn btn-sm" data-hire="${esc(s.id)}" data-hirename="${esc(s.name||s.id)}">Hire</button>
|
|
452
590
|
</div>`;
|
|
453
591
|
}).join('');
|
|
@@ -464,7 +602,7 @@ function _buildHireSelectors() {
|
|
|
464
602
|
const ops = (S.synapseHealth || []);
|
|
465
603
|
const teams = [...new Set(ops.map(o => o.team).filter(Boolean))];
|
|
466
604
|
const opOptions = [`<option value="">— none (team lead) —</option>`,
|
|
467
|
-
...ops.map(o => `<option value="${esc(o.id||o.operativeId)}">${esc(o
|
|
605
|
+
...ops.map(o => `<option value="${esc(o.id||o.operativeId)}">${esc(opLabel(o))}</option>`)
|
|
468
606
|
].join('');
|
|
469
607
|
const teamOptions = [`<option value="">— solo —</option>`,
|
|
470
608
|
...teams.map(t => `<option value="${esc(t)}">${esc(t)}</option>`)
|
|
@@ -643,9 +781,9 @@ function _openOpDrawer(id, name) {
|
|
|
643
781
|
// Render any active push-mode dispatches for this operative
|
|
644
782
|
const activeRuns = [..._dispatches.values()].filter(r => r.operativeId === opId);
|
|
645
783
|
const stripInner = activeRuns.map(_buildDispatchStrip).join('');
|
|
646
|
-
openDrawer({ title: op
|
|
784
|
+
openDrawer({ title: opLabel(op)||'Operative',
|
|
647
785
|
body: `<div class="dsec"><div class="dsec-title">Operative</div>${drows([
|
|
648
|
-
['ID',op.id||op.operativeId],['Role',op.role],['Status',op.status],
|
|
786
|
+
['ID',op.id||op.operativeId],['Name',opLabel(op)],['Role',op.role],['Status',op.status],
|
|
649
787
|
['Budget used',op.budgetUsed!=null?fmtNum(op.budgetUsed):'—'],
|
|
650
788
|
['Budget alloc',op.budget!=null?fmtNum(op.budget):'—'],
|
|
651
789
|
['Team',op.team||op.strikeName||'—']
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-prime",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.31",
|
|
4
4
|
"description": "Local-first MCP control plane for coding agents with bootstrap-orchestrate execution, memory fabric, token budgeting, and worktree-backed swarms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|