@yemi33/minions 0.1.2178 → 0.1.2179
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/bin/minions.js +24 -11
- package/dashboard/js/command-parser.js +1 -1
- package/dashboard/js/memory-panel.js +262 -0
- package/dashboard/js/qa.js +2 -2
- package/dashboard/js/refresh.js +1 -1
- package/dashboard/js/render-other.js +1 -1
- package/dashboard/js/render-prs.js +2 -1
- package/dashboard/js/render-schedules.js +1 -1
- package/dashboard/js/render-watches.js +1 -1
- package/dashboard/pages/engine-memory-panel.html +49 -0
- package/dashboard/pages/engine.html +1 -0
- package/dashboard/slim/js/link-pr.js +5 -5
- package/dashboard/slim/js/modals-tiles.js +44 -3
- package/dashboard/slim/js/projects.js +8 -6
- package/dashboard/slim/styles.css +20 -0
- package/dashboard-build.js +17 -2
- package/dashboard.js +227 -16
- package/docs/branch-derivation.md +13 -1
- package/engine/diagnostics-memory.js +190 -0
- package/engine/lifecycle.js +111 -1
- package/engine/queries.js +139 -4
- package/engine/shared.js +113 -2
- package/engine/watchdog.js +6 -0
- package/engine.js +45 -1
- package/package.json +2 -2
package/bin/minions.js
CHANGED
|
@@ -217,9 +217,9 @@ async function _waitForBrowserReconnect(minionsHome, { afterMs, timeoutMs = 5000
|
|
|
217
217
|
return false;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
function _openInBrowser(url) {
|
|
221
|
-
const result = openUrlInBrowser(url);
|
|
222
|
-
if (!result.ok) {
|
|
220
|
+
function _openInBrowser(url, reason, callerHint) {
|
|
221
|
+
const result = openUrlInBrowser(url, { reason, callerHint });
|
|
222
|
+
if (!result.ok && !result.suppressed) {
|
|
223
223
|
console.log(` Could not auto-open browser: ${result.error}`);
|
|
224
224
|
console.log(` Please open ${url} manually.`);
|
|
225
225
|
}
|
|
@@ -400,11 +400,15 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
|
|
|
400
400
|
}
|
|
401
401
|
console.log(` Restart verified: engine PID ${result.engine.pid}; dashboard healthy.`);
|
|
402
402
|
|
|
403
|
-
const shouldOpen = forceOpen ||
|
|
404
|
-
|
|
403
|
+
const shouldOpen = forceOpen || (
|
|
404
|
+
process.env.MINIONS_NO_AUTO_OPEN !== '1' && (
|
|
405
|
+
!dashWasUp ||
|
|
406
|
+
!(await _waitForBrowserReconnect(MINIONS_HOME, { afterMs: restartStartMs, timeoutMs: 5000 }))
|
|
407
|
+
)
|
|
408
|
+
);
|
|
405
409
|
if (shouldOpen) {
|
|
406
410
|
console.log(` Opening dashboard in browser...`);
|
|
407
|
-
_openInBrowser(`http://localhost:${actualPort}
|
|
411
|
+
_openInBrowser(`http://localhost:${actualPort}`, 'cli-restart-no-beacon', 'bin/minions.js:407');
|
|
408
412
|
}
|
|
409
413
|
console.log('');
|
|
410
414
|
})().catch(err => {
|
|
@@ -861,11 +865,20 @@ function init() {
|
|
|
861
865
|
|
|
862
866
|
void (async () => {
|
|
863
867
|
const actualPort = await _waitForDashboardPortFile(MINIONS_HOME, 8000) || upgradeRequested.port;
|
|
864
|
-
|
|
865
|
-
|
|
868
|
+
// W-mqb9y83o — same hard kill-switch as spawnFullStackAndVerify:
|
|
869
|
+
// MINIONS_NO_AUTO_OPEN=1 in the parent env (set by the watchdog spawn
|
|
870
|
+
// and /api/dashboard/restart spawn) suppresses auto-open regardless of
|
|
871
|
+
// the beacon heuristic. `forceOpen` (`--open` / MINIONS_FORCE_OPEN=1)
|
|
872
|
+
// still wins as the explicit operator override.
|
|
873
|
+
const shouldOpen = forceOpen || (
|
|
874
|
+
process.env.MINIONS_NO_AUTO_OPEN !== '1' && (
|
|
875
|
+
!dashWasUp ||
|
|
876
|
+
!(await _waitForBrowserReconnect(MINIONS_HOME, { afterMs: restartStartMs, timeoutMs: 5000 }))
|
|
877
|
+
)
|
|
878
|
+
);
|
|
866
879
|
if (shouldOpen) {
|
|
867
880
|
console.log(` Opening dashboard in browser...`);
|
|
868
|
-
_openInBrowser(`http://localhost:${actualPort}
|
|
881
|
+
_openInBrowser(`http://localhost:${actualPort}`, 'cli-upgrade-no-beacon', 'bin/minions.js:868');
|
|
869
882
|
}
|
|
870
883
|
})().catch(err => {
|
|
871
884
|
console.log(` Could not open dashboard: ${err.message}`);
|
|
@@ -1209,7 +1222,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1209
1222
|
console.log(`\n Minions is already running (engine PID ${enginePid}; dashboard http://localhost:${startResolved.port}).`);
|
|
1210
1223
|
if (forceOpen) {
|
|
1211
1224
|
console.log(` Opening dashboard in browser...`);
|
|
1212
|
-
_openInBrowser(`http://localhost:${startResolved.port}
|
|
1225
|
+
_openInBrowser(`http://localhost:${startResolved.port}`, 'cli-start-force-open', 'bin/minions.js:1212');
|
|
1213
1226
|
} else {
|
|
1214
1227
|
console.log(` Run \`minions dash\` to open the dashboard, or \`minions start --open\` to force a new browser tab.\n`);
|
|
1215
1228
|
}
|
|
@@ -1547,7 +1560,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1547
1560
|
handled = true;
|
|
1548
1561
|
const url = `http://localhost:${dashResolved.port}`;
|
|
1549
1562
|
console.log(`\n Dashboard already running: ${url}\n`);
|
|
1550
|
-
openUrlInBrowser(url);
|
|
1563
|
+
openUrlInBrowser(url, { reason: 'cli-dash-warm', callerHint: 'bin/minions.js:1559' });
|
|
1551
1564
|
});
|
|
1552
1565
|
sock.on('error', () => {
|
|
1553
1566
|
sock.destroy();
|
|
@@ -8,7 +8,7 @@ function cmdUpdateAgentList(agents) {
|
|
|
8
8
|
cmdAgents = (agents || []).map(a => ({ id: a.id, name: a.name, emoji: a.emoji, role: a.role }));
|
|
9
9
|
}
|
|
10
10
|
function cmdUpdateProjectList(projects) {
|
|
11
|
-
cmdProjects = (projects || []).map(p => ({ name: p.name, description: p.description || '' }));
|
|
11
|
+
cmdProjects = (projects || []).map(p => ({ name: p.name, displayName: p.displayName || p.name, description: p.description || '' }));
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function showToast(id, msg, ok, durationMs) {
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// dashboard/js/memory-panel.js — Memory panel poller + inline SVG sparkline.
|
|
2
|
+
// P-d4e5f6a7 (memory + perf audit plan, dashboard surface).
|
|
3
|
+
//
|
|
4
|
+
// Lifecycle: mountMemoryPanel() registers two intervals — a 10 s poll against
|
|
5
|
+
// /api/diagnostics/memory for live values, and a 60 s poll against
|
|
6
|
+
// /api/diagnostics/memory/history?process={engine,dashboard} for the inline
|
|
7
|
+
// SVG sparkline (RSS + heapUsed over the last hour). unmountMemoryPanel()
|
|
8
|
+
// clears both intervals.
|
|
9
|
+
//
|
|
10
|
+
// Mount/unmount registration uses the canonical PAGE_LAZY_LOADERS +
|
|
11
|
+
// PAGE_LEAVE_HOOKS maps declared in state.js — engine page enter triggers
|
|
12
|
+
// mount, every page-leave call triggers unmount (idempotent). state.js
|
|
13
|
+
// evaluates earlier in the assembled bundle so the maps already exist when
|
|
14
|
+
// this file pushes its hooks; the initial switchPage(currentPage) call at
|
|
15
|
+
// the end of refresh.js (last file in the bundle) is what fires the first
|
|
16
|
+
// mount when a user direct-loads /engine.
|
|
17
|
+
//
|
|
18
|
+
// XSS safety: nothing here writes into innerHTML / adjacent-html sinks. Live
|
|
19
|
+
// values land via textContent; the sparkline is constructed via
|
|
20
|
+
// createElementNS so it stays clean against eslint-plugin-no-unsanitized.
|
|
21
|
+
|
|
22
|
+
const MEMORY_POLL_LIVE_MS = 10_000;
|
|
23
|
+
const MEMORY_POLL_HISTORY_MS = 60_000;
|
|
24
|
+
const MEMORY_SPARKLINE_WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
|
25
|
+
|
|
26
|
+
let _memoryPanelLiveInterval = null;
|
|
27
|
+
let _memoryPanelHistoryInterval = null;
|
|
28
|
+
let _memoryPanelMounted = false;
|
|
29
|
+
|
|
30
|
+
function _memFmtBytes(n) {
|
|
31
|
+
if (!Number.isFinite(n)) return '—';
|
|
32
|
+
const mb = n / (1024 * 1024);
|
|
33
|
+
if (mb < 1024) return mb.toFixed(1) + ' MB';
|
|
34
|
+
return (mb / 1024).toFixed(2) + ' GB';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _memFmtMs(n) {
|
|
38
|
+
if (!Number.isFinite(n)) return '—';
|
|
39
|
+
if (n < 1) return n.toFixed(2) + ' ms';
|
|
40
|
+
if (n < 100) return n.toFixed(1) + ' ms';
|
|
41
|
+
return Math.round(n) + ' ms';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function _memFmtUptime(seconds) {
|
|
45
|
+
if (!Number.isFinite(seconds) || seconds < 0) return '—';
|
|
46
|
+
const s = Math.floor(seconds);
|
|
47
|
+
if (s < 60) return s + 's';
|
|
48
|
+
const m = Math.floor(s / 60);
|
|
49
|
+
if (m < 60) return m + 'm ' + (s % 60) + 's';
|
|
50
|
+
const h = Math.floor(m / 60);
|
|
51
|
+
if (h < 24) return h + 'h ' + (m % 60) + 'm';
|
|
52
|
+
const d = Math.floor(h / 24);
|
|
53
|
+
return d + 'd ' + (h % 24) + 'h';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _memSetText(id, val) {
|
|
57
|
+
const el = document.getElementById(id);
|
|
58
|
+
if (el) el.textContent = val;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _memRenderCard(prefix, sample, stale) {
|
|
62
|
+
if (!sample) {
|
|
63
|
+
const fallback = stale ? 'stale' : '—';
|
|
64
|
+
_memSetText('memory-' + prefix + '-rss', fallback);
|
|
65
|
+
_memSetText('memory-' + prefix + '-heap', '—');
|
|
66
|
+
_memSetText('memory-' + prefix + '-external', '—');
|
|
67
|
+
_memSetText('memory-' + prefix + '-lag', '—');
|
|
68
|
+
_memSetText('memory-' + prefix + '-gc', '—');
|
|
69
|
+
_memSetText('memory-' + prefix + '-uptime', '—');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
_memSetText('memory-' + prefix + '-rss', _memFmtBytes(sample.rss));
|
|
73
|
+
_memSetText('memory-' + prefix + '-heap', _memFmtBytes(sample.heapUsed) + ' / ' + _memFmtBytes(sample.heapTotal));
|
|
74
|
+
_memSetText('memory-' + prefix + '-external', _memFmtBytes(sample.external));
|
|
75
|
+
_memSetText('memory-' + prefix + '-lag', _memFmtMs(sample.eventLoopLagP50) + ' / ' + _memFmtMs(sample.eventLoopLagP99));
|
|
76
|
+
const gcVal = (Number.isFinite(sample.lastGcPauseMs) && sample.lastGcPauseMs > 0)
|
|
77
|
+
? _memFmtMs(sample.lastGcPauseMs) + (sample.lastGcKind ? ' (' + sample.lastGcKind + ')' : '')
|
|
78
|
+
: '—';
|
|
79
|
+
_memSetText('memory-' + prefix + '-gc', gcVal);
|
|
80
|
+
_memSetText('memory-' + prefix + '-uptime', _memFmtUptime(sample.uptime));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _memSetStaleBadge(id, stale) {
|
|
84
|
+
const el = document.getElementById(id);
|
|
85
|
+
if (!el) return;
|
|
86
|
+
el.style.display = stale ? '' : 'none';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Pure builder: returns an <svg> element rendering two polylines (RSS +
|
|
90
|
+
// heapUsed) over the last MEMORY_SPARKLINE_WINDOW_MS of samples, or a
|
|
91
|
+
// "no samples" label when the buffer is empty. Pure DOM via createElementNS
|
|
92
|
+
// keeps eslint-plugin-no-unsanitized happy.
|
|
93
|
+
function _memBuildSparkline(samples, opts) {
|
|
94
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
95
|
+
const width = (opts && opts.width) || 320;
|
|
96
|
+
const height = (opts && opts.height) || 60;
|
|
97
|
+
const pad = 2;
|
|
98
|
+
|
|
99
|
+
const svg = document.createElementNS(svgNS, 'svg');
|
|
100
|
+
svg.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
|
|
101
|
+
svg.setAttribute('preserveAspectRatio', 'none');
|
|
102
|
+
svg.setAttribute('width', '100%');
|
|
103
|
+
svg.setAttribute('height', String(height));
|
|
104
|
+
svg.style.display = 'block';
|
|
105
|
+
|
|
106
|
+
if (!Array.isArray(samples) || samples.length === 0) {
|
|
107
|
+
const label = document.createElementNS(svgNS, 'text');
|
|
108
|
+
label.setAttribute('x', String(width / 2));
|
|
109
|
+
label.setAttribute('y', String(height / 2));
|
|
110
|
+
label.setAttribute('text-anchor', 'middle');
|
|
111
|
+
label.setAttribute('dominant-baseline', 'middle');
|
|
112
|
+
label.setAttribute('fill', 'currentColor');
|
|
113
|
+
label.setAttribute('opacity', '0.5');
|
|
114
|
+
label.setAttribute('font-size', '11');
|
|
115
|
+
label.textContent = 'no samples yet';
|
|
116
|
+
svg.appendChild(label);
|
|
117
|
+
return svg;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Clip to the last hour, falling back to the full ring when nothing recent.
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const tMin = now - MEMORY_SPARKLINE_WINDOW_MS;
|
|
123
|
+
const windowed = samples.filter(function (s) {
|
|
124
|
+
return s && Number.isFinite(s.capturedAt) && s.capturedAt >= tMin;
|
|
125
|
+
});
|
|
126
|
+
const useSamples = windowed.length ? windowed : samples.filter(function (s) {
|
|
127
|
+
return s && Number.isFinite(s.capturedAt);
|
|
128
|
+
});
|
|
129
|
+
if (!useSamples.length) return svg;
|
|
130
|
+
|
|
131
|
+
const t0 = useSamples[0].capturedAt;
|
|
132
|
+
const t1 = useSamples[useSamples.length - 1].capturedAt;
|
|
133
|
+
const tSpan = Math.max(1, t1 - t0);
|
|
134
|
+
|
|
135
|
+
let yMin = Infinity;
|
|
136
|
+
let yMax = -Infinity;
|
|
137
|
+
for (const s of useSamples) {
|
|
138
|
+
for (const v of [s.rss, s.heapUsed]) {
|
|
139
|
+
if (Number.isFinite(v)) {
|
|
140
|
+
if (v < yMin) yMin = v;
|
|
141
|
+
if (v > yMax) yMax = v;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!Number.isFinite(yMin) || !Number.isFinite(yMax)) { yMin = 0; yMax = 1; }
|
|
146
|
+
const ySpan = Math.max(1, yMax - yMin);
|
|
147
|
+
|
|
148
|
+
function _series(field) {
|
|
149
|
+
const pts = [];
|
|
150
|
+
for (const s of useSamples) {
|
|
151
|
+
const v = s[field];
|
|
152
|
+
if (!Number.isFinite(v)) continue;
|
|
153
|
+
const x = pad + ((s.capturedAt - t0) / tSpan) * (width - 2 * pad);
|
|
154
|
+
const y = height - pad - ((v - yMin) / ySpan) * (height - 2 * pad);
|
|
155
|
+
pts.push(x.toFixed(1) + ',' + y.toFixed(1));
|
|
156
|
+
}
|
|
157
|
+
return pts.join(' ');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function _addLine(field, color) {
|
|
161
|
+
const pts = _series(field);
|
|
162
|
+
if (!pts) return;
|
|
163
|
+
const line = document.createElementNS(svgNS, 'polyline');
|
|
164
|
+
line.setAttribute('points', pts);
|
|
165
|
+
line.setAttribute('fill', 'none');
|
|
166
|
+
line.setAttribute('stroke', color);
|
|
167
|
+
line.setAttribute('stroke-width', '1.5');
|
|
168
|
+
line.setAttribute('vector-effect', 'non-scaling-stroke');
|
|
169
|
+
svg.appendChild(line);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_addLine('rss', 'var(--blue, #4ea1ff)');
|
|
173
|
+
_addLine('heapUsed', 'var(--green, #4caf50)');
|
|
174
|
+
return svg;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function _memRenderSparkline(containerId, samples) {
|
|
178
|
+
const el = document.getElementById(containerId);
|
|
179
|
+
if (!el) return;
|
|
180
|
+
const width = el.clientWidth || 320;
|
|
181
|
+
const svg = _memBuildSparkline(samples, { width, height: 60 });
|
|
182
|
+
el.replaceChildren(svg);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function _memRefreshLive() {
|
|
186
|
+
let data;
|
|
187
|
+
try {
|
|
188
|
+
const res = await fetch('/api/diagnostics/memory');
|
|
189
|
+
if (!res.ok) return;
|
|
190
|
+
data = await res.json();
|
|
191
|
+
} catch {
|
|
192
|
+
return; // network blip — leave previous values in place
|
|
193
|
+
}
|
|
194
|
+
if (!data || typeof data !== 'object') return;
|
|
195
|
+
_memRenderCard('engine', data.engine, !!data.engineStale);
|
|
196
|
+
_memRenderCard('dashboard', data.dashboard, false);
|
|
197
|
+
_memSetStaleBadge('memory-engine-stale-badge', !!data.engineStale);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function _memRefreshHistory() {
|
|
201
|
+
await Promise.all(['engine', 'dashboard'].map(async function (proc) {
|
|
202
|
+
try {
|
|
203
|
+
const res = await fetch('/api/diagnostics/memory/history?process=' + encodeURIComponent(proc));
|
|
204
|
+
if (!res.ok) return;
|
|
205
|
+
const data = await res.json();
|
|
206
|
+
const samples = (data && Array.isArray(data.samples)) ? data.samples : [];
|
|
207
|
+
_memRenderSparkline('memory-' + proc + '-sparkline', samples);
|
|
208
|
+
} catch {
|
|
209
|
+
// network blip — keep the previous sparkline
|
|
210
|
+
}
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function mountMemoryPanel() {
|
|
215
|
+
if (_memoryPanelMounted) return;
|
|
216
|
+
// No-op when the engine page hasn't been assembled yet (defensive — the
|
|
217
|
+
// fragment is always part of the bundle, but this keeps the hook safe
|
|
218
|
+
// against partial DOM states during test harnesses).
|
|
219
|
+
if (!document.getElementById('memory-panel-content')) return;
|
|
220
|
+
_memoryPanelMounted = true;
|
|
221
|
+
_memRefreshLive();
|
|
222
|
+
_memRefreshHistory();
|
|
223
|
+
_memoryPanelLiveInterval = setInterval(_memRefreshLive, MEMORY_POLL_LIVE_MS);
|
|
224
|
+
_memoryPanelHistoryInterval = setInterval(_memRefreshHistory, MEMORY_POLL_HISTORY_MS);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function unmountMemoryPanel() {
|
|
228
|
+
if (!_memoryPanelMounted) return;
|
|
229
|
+
_memoryPanelMounted = false;
|
|
230
|
+
if (_memoryPanelLiveInterval) {
|
|
231
|
+
clearInterval(_memoryPanelLiveInterval);
|
|
232
|
+
_memoryPanelLiveInterval = null;
|
|
233
|
+
}
|
|
234
|
+
if (_memoryPanelHistoryInterval) {
|
|
235
|
+
clearInterval(_memoryPanelHistoryInterval);
|
|
236
|
+
_memoryPanelHistoryInterval = null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Register the lifecycle hooks against the canonical maps in state.js.
|
|
241
|
+
// `const` declarations don't block mutating the underlying object/array,
|
|
242
|
+
// so we can splice in our entries without editing state.js. Best-effort:
|
|
243
|
+
// any failure leaves the panel unmounted but does not break navigation.
|
|
244
|
+
try {
|
|
245
|
+
if (typeof PAGE_LAZY_LOADERS === 'object' && PAGE_LAZY_LOADERS) {
|
|
246
|
+
if (!Array.isArray(PAGE_LAZY_LOADERS.engine)) PAGE_LAZY_LOADERS.engine = [];
|
|
247
|
+
if (PAGE_LAZY_LOADERS.engine.indexOf('mountMemoryPanel') < 0) {
|
|
248
|
+
PAGE_LAZY_LOADERS.engine.push('mountMemoryPanel');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (typeof PAGE_LEAVE_HOOKS !== 'undefined' && Array.isArray(PAGE_LEAVE_HOOKS)) {
|
|
252
|
+
if (PAGE_LEAVE_HOOKS.indexOf('unmountMemoryPanel') < 0) {
|
|
253
|
+
PAGE_LEAVE_HOOKS.push('unmountMemoryPanel');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} catch { /* registration is best-effort */ }
|
|
257
|
+
|
|
258
|
+
window.MinionsMemoryPanel = {
|
|
259
|
+
mountMemoryPanel,
|
|
260
|
+
unmountMemoryPanel,
|
|
261
|
+
_buildSparkline: _memBuildSparkline,
|
|
262
|
+
};
|
package/dashboard/js/qa.js
CHANGED
|
@@ -620,7 +620,7 @@ async function loadQaProjectsSelect() {
|
|
|
620
620
|
const res = await fetch('/api/status');
|
|
621
621
|
const json = res.ok ? await res.json() : {};
|
|
622
622
|
if (Array.isArray(json && json.projects)) {
|
|
623
|
-
projects = json.projects.map(p => ({ name: p.name, description: p.description || '' }));
|
|
623
|
+
projects = json.projects.map(p => ({ name: p.name, displayName: p.displayName || p.name, description: p.description || '' }));
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
626
|
} catch { projects = []; }
|
|
@@ -631,7 +631,7 @@ async function loadQaProjectsSelect() {
|
|
|
631
631
|
if (!p || !p.name) continue;
|
|
632
632
|
const opt = document.createElement('option');
|
|
633
633
|
opt.value = p.name;
|
|
634
|
-
opt.textContent = p.name;
|
|
634
|
+
opt.textContent = p.displayName || p.name;
|
|
635
635
|
if (previouslySelected.has(p.name)) opt.selected = true;
|
|
636
636
|
sel.appendChild(opt);
|
|
637
637
|
}
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -12,7 +12,7 @@ function renderProjects(projects) {
|
|
|
12
12
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: project name, path, branch metadata)
|
|
13
13
|
list.innerHTML = visible.map(p =>
|
|
14
14
|
'<span data-project="' + escHtml(p.name) + '" title="' + escHtml(p.path || '') + '" style="display:inline-flex;align-items:center;gap:6px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--blue);font-weight:500;cursor:help">' +
|
|
15
|
-
escHtml(p.name) +
|
|
15
|
+
escHtml(p.displayName || p.name) +
|
|
16
16
|
_renderProjectBranch(p) +
|
|
17
17
|
'<span onclick="event.stopPropagation();projectChipRemove(\'' + escHtml(p.name) + '\')" title="Remove project (cancels pending work, archives data dir)" style="color:var(--muted);font-weight:600;cursor:pointer;padding:0 2px;line-height:1" onmouseover="this.style.color=\'var(--red)\'" onmouseout="this.style.color=\'var(--muted)\'">×</span>' +
|
|
18
18
|
'</span>'
|
|
@@ -320,7 +320,8 @@ function openAddPrModal() {
|
|
|
320
320
|
const projects = (typeof cmdProjects !== 'undefined' ? cmdProjects : []) || [];
|
|
321
321
|
const projOpts = projects.map(p => {
|
|
322
322
|
const name = typeof p === 'object' ? p.name : p;
|
|
323
|
-
|
|
323
|
+
const label = (typeof p === 'object' && (p.displayName || p.name)) || name;
|
|
324
|
+
return '<option value="' + escapeHtml(name) + '">' + escapeHtml(label) + '</option>';
|
|
324
325
|
}).join('');
|
|
325
326
|
const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
|
|
326
327
|
|
|
@@ -422,7 +422,7 @@ function _scheduleFormHtml(sched, isEdit) {
|
|
|
422
422
|
const priorities = ['high', 'medium', 'low'];
|
|
423
423
|
const typeOpts = types.map(t => '<option value="' + t + '"' + ((sched.type || 'implement') === t ? ' selected' : '') + '>' + t + '</option>').join('');
|
|
424
424
|
const priOpts = priorities.map(p => '<option value="' + p + '"' + ((sched.priority || 'medium') === p ? ' selected' : '') + '>' + p + '</option>').join('');
|
|
425
|
-
const projOpts = '<option value="">Any</option>' + (cmdProjects || []).map(p => '<option value="' + escHtml(p.name) + '"' + (sched.project === p.name ? ' selected' : '') + '>' + escHtml(p.name) + '</option>').join('');
|
|
425
|
+
const projOpts = '<option value="">Any</option>' + (cmdProjects || []).map(p => '<option value="' + escHtml(p.name) + '"' + (sched.project === p.name ? ' selected' : '') + '>' + escHtml(p.displayName || p.name) + '</option>').join('');
|
|
426
426
|
const agentOpts = '<option value="">Auto</option>' + (cmdAgents || []).map(a => '<option value="' + escHtml(a.id) + '"' + (sched.agent === a.id ? ' selected' : '') + '>' + escHtml(a.name) + '</option>').join('');
|
|
427
427
|
|
|
428
428
|
const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
|
|
@@ -486,7 +486,7 @@ function _watchFormHtml() {
|
|
|
486
486
|
}).join('');
|
|
487
487
|
|
|
488
488
|
var agentOpts = '<option value="">human</option>' + (cmdAgents || []).map(function(a) { return '<option value="' + escHtml(a.id) + '">' + escHtml(a.name) + '</option>'; }).join('');
|
|
489
|
-
var projOpts = '<option value="">Any</option>' + (cmdProjects || []).map(function(p) { return '<option value="' + escHtml(p.name) + '">' + escHtml(p.name) + '</option>'; }).join('');
|
|
489
|
+
var projOpts = '<option value="">Any</option>' + (cmdProjects || []).map(function(p) { return '<option value="' + escHtml(p.name) + '">' + escHtml(p.displayName || p.name) + '</option>'; }).join('');
|
|
490
490
|
|
|
491
491
|
return '<div style="display:flex;flex-direction:column;gap:12px;font-family:inherit">' +
|
|
492
492
|
'<div id="watch-form-error" style="display:none;color:var(--red);font-size:var(--text-md);padding:6px 10px;background:rgba(255,50,50,0.1);border-radius:var(--radius-sm)"></div>' +
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
dashboard/pages/engine-memory-panel.html — Memory panel fragment (P-d4e5f6a7).
|
|
3
|
+
|
|
4
|
+
Side-by-side cards for the engine and dashboard processes. Live values are
|
|
5
|
+
refreshed every 10 s by dashboard/js/memory-panel.js (GET /api/diagnostics/memory);
|
|
6
|
+
the inline SVG sparkline is refreshed every 60 s
|
|
7
|
+
(GET /api/diagnostics/memory/history?process=...). The yellow
|
|
8
|
+
"engine sample stale" badge is unhidden by the JS module when
|
|
9
|
+
engineStale === true.
|
|
10
|
+
|
|
11
|
+
dashboard-build.js substitutes this fragment into dashboard/pages/engine.html
|
|
12
|
+
at assembly time (see pageSubFragments).
|
|
13
|
+
-->
|
|
14
|
+
<section id="memory-panel-section">
|
|
15
|
+
<h2>Memory <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">RSS / heap / event-loop / GC for engine + dashboard processes</span></h2>
|
|
16
|
+
<div id="memory-panel-content" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:12px">
|
|
17
|
+
<div id="memory-card-engine" class="memory-card" style="border:1px solid var(--border);border-radius:4px;padding:10px;background:var(--surface2)">
|
|
18
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
|
|
19
|
+
<div style="font-weight:600;font-size:var(--text-md);font-family:monospace">engine</div>
|
|
20
|
+
<span id="memory-engine-stale-badge" style="display:none;background:var(--yellow);color:#000;padding:2px 8px;border-radius:3px;font-size:var(--text-sm);font-weight:600">engine sample stale</span>
|
|
21
|
+
</div>
|
|
22
|
+
<dl style="display:grid;grid-template-columns:max-content 1fr;gap:3px 12px;margin:0 0 8px 0;font-size:var(--text-base)">
|
|
23
|
+
<dt style="color:var(--muted);margin:0">RSS</dt><dd id="memory-engine-rss" style="margin:0;font-family:monospace">—</dd>
|
|
24
|
+
<dt style="color:var(--muted);margin:0">Heap used / total</dt><dd id="memory-engine-heap" style="margin:0;font-family:monospace">—</dd>
|
|
25
|
+
<dt style="color:var(--muted);margin:0">External</dt><dd id="memory-engine-external" style="margin:0;font-family:monospace">—</dd>
|
|
26
|
+
<dt style="color:var(--muted);margin:0">Event-loop lag p50 / p99</dt><dd id="memory-engine-lag" style="margin:0;font-family:monospace">—</dd>
|
|
27
|
+
<dt style="color:var(--muted);margin:0">Last GC pause</dt><dd id="memory-engine-gc" style="margin:0;font-family:monospace">—</dd>
|
|
28
|
+
<dt style="color:var(--muted);margin:0">Uptime</dt><dd id="memory-engine-uptime" style="margin:0;font-family:monospace">—</dd>
|
|
29
|
+
</dl>
|
|
30
|
+
<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:2px"><span style="color:var(--blue,#4ea1ff)">●</span> RSS <span style="color:var(--green,#4caf50)">●</span> heapUsed <span style="opacity:0.7">(last hour)</span></div>
|
|
31
|
+
<div id="memory-engine-sparkline" style="height:60px;width:100%;color:var(--fg)"></div>
|
|
32
|
+
</div>
|
|
33
|
+
<div id="memory-card-dashboard" class="memory-card" style="border:1px solid var(--border);border-radius:4px;padding:10px;background:var(--surface2)">
|
|
34
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
|
|
35
|
+
<div style="font-weight:600;font-size:var(--text-md);font-family:monospace">dashboard</div>
|
|
36
|
+
</div>
|
|
37
|
+
<dl style="display:grid;grid-template-columns:max-content 1fr;gap:3px 12px;margin:0 0 8px 0;font-size:var(--text-base)">
|
|
38
|
+
<dt style="color:var(--muted);margin:0">RSS</dt><dd id="memory-dashboard-rss" style="margin:0;font-family:monospace">—</dd>
|
|
39
|
+
<dt style="color:var(--muted);margin:0">Heap used / total</dt><dd id="memory-dashboard-heap" style="margin:0;font-family:monospace">—</dd>
|
|
40
|
+
<dt style="color:var(--muted);margin:0">External</dt><dd id="memory-dashboard-external" style="margin:0;font-family:monospace">—</dd>
|
|
41
|
+
<dt style="color:var(--muted);margin:0">Event-loop lag p50 / p99</dt><dd id="memory-dashboard-lag" style="margin:0;font-family:monospace">—</dd>
|
|
42
|
+
<dt style="color:var(--muted);margin:0">Last GC pause</dt><dd id="memory-dashboard-gc" style="margin:0;font-family:monospace">—</dd>
|
|
43
|
+
<dt style="color:var(--muted);margin:0">Uptime</dt><dd id="memory-dashboard-uptime" style="margin:0;font-family:monospace">—</dd>
|
|
44
|
+
</dl>
|
|
45
|
+
<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:2px"><span style="color:var(--blue,#4ea1ff)">●</span> RSS <span style="color:var(--green,#4caf50)">●</span> heapUsed <span style="opacity:0.7">(last hour)</span></div>
|
|
46
|
+
<div id="memory-dashboard-sparkline" style="height:60px;width:100%;color:var(--fg)"></div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<section>
|
|
4
4
|
<div id="engine-quick-stats" style="display:flex;gap:16px;margin-bottom:12px;font-size:var(--text-base);color:var(--muted)"></div>
|
|
5
5
|
</section>
|
|
6
|
+
<!-- __ENGINE_MEMORY_PANEL__ -->
|
|
6
7
|
<section>
|
|
7
8
|
<h2>Engine Log <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">tick-by-tick audit trail of engine operations</span></h2>
|
|
8
9
|
<div class="log-list" id="engine-log">No log entries yet.</div>
|
|
@@ -6,18 +6,18 @@
|
|
|
6
6
|
var modal = document.getElementById('slim-linkpr-modal');
|
|
7
7
|
if (!modal) return;
|
|
8
8
|
var projects = (lastStatusData && Array.isArray(lastStatusData.projects) ? lastStatusData.projects : [])
|
|
9
|
-
.
|
|
10
|
-
.
|
|
9
|
+
.filter(function(p) { return p && p.name; })
|
|
10
|
+
.map(function(p) { return { name: String(p.name), label: String(p.displayName || p.name) }; });
|
|
11
11
|
var sel = document.getElementById('slim-linkpr-project');
|
|
12
12
|
sel.textContent = '';
|
|
13
13
|
var auto = document.createElement('option');
|
|
14
14
|
auto.value = '';
|
|
15
15
|
auto.textContent = 'Auto-detect from URL (central if no unique match)';
|
|
16
16
|
sel.appendChild(auto);
|
|
17
|
-
projects.forEach(function(
|
|
17
|
+
projects.forEach(function(proj) {
|
|
18
18
|
var o = document.createElement('option');
|
|
19
|
-
o.value = name;
|
|
20
|
-
o.textContent =
|
|
19
|
+
o.value = proj.name;
|
|
20
|
+
o.textContent = proj.label;
|
|
21
21
|
sel.appendChild(o);
|
|
22
22
|
});
|
|
23
23
|
var warn = document.getElementById('slim-linkpr-noproj');
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
}
|
|
160
160
|
var prs = Array.isArray(data.pullRequests) ? data.pullRequests : [];
|
|
161
161
|
if (!prs.length) { tileEmpty(body, 'No tracked pull requests.'); return; }
|
|
162
|
-
|
|
162
|
+
function prCard(p) {
|
|
163
163
|
var num = p.prNumber || p.number || p.id;
|
|
164
164
|
var build = p.buildStatus || '';
|
|
165
165
|
var review = p.reviewStatus || '';
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
: ((p.status === 'active' || p.status === 'linked') ? 'blue' : ''));
|
|
169
169
|
var statusText = document.createElement('span');
|
|
170
170
|
statusText.textContent = p.status || 'unknown';
|
|
171
|
-
|
|
171
|
+
return tileCard({
|
|
172
172
|
title: (num ? '#' + num + ' ' : '') + (p.title || '(untitled PR)'),
|
|
173
173
|
// Project slug first (under title), then build/review row below — the
|
|
174
174
|
// project is the strongest disambiguator across PRs and benefits from
|
|
@@ -178,7 +178,48 @@
|
|
|
178
178
|
metaNodes: [statusText, statusSpan('build', build, BUILD_STATUS), statusSpan('review', review, REVIEW_STATUS)],
|
|
179
179
|
chip: { text: p.status || '—', cls: cls },
|
|
180
180
|
href: p.url || null,
|
|
181
|
-
})
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// Partition once into three fixed buckets (status normalized to lowercase),
|
|
184
|
+
// preserving each PR's relative order within its bucket. Mirrors shared
|
|
185
|
+
// PR_STATUS (active/merged/abandoned/closed/linked); anything that does not
|
|
186
|
+
// match the merged/abandoned buckets defaults to Active so a PR with an
|
|
187
|
+
// unexpected status never silently disappears.
|
|
188
|
+
var buckets = { active: [], merged: [], abandoned: [] };
|
|
189
|
+
prs.forEach(function(p) {
|
|
190
|
+
var s = String(p.status || '').toLowerCase();
|
|
191
|
+
if (s === 'abandoned') buckets.abandoned.push(p);
|
|
192
|
+
else if (s === 'merged' || s === 'completed' || s === 'closed') buckets.merged.push(p);
|
|
193
|
+
else buckets.active.push(p);
|
|
194
|
+
});
|
|
195
|
+
// Fixed section order: Active (open) → Merged / Completed → Abandoned.
|
|
196
|
+
// Empty sections are omitted entirely.
|
|
197
|
+
var sections = [
|
|
198
|
+
{ label: 'Active', prs: buckets.active, open: true },
|
|
199
|
+
{ label: 'Merged / Completed', prs: buckets.merged, open: false },
|
|
200
|
+
{ label: 'Abandoned', prs: buckets.abandoned, open: false },
|
|
201
|
+
];
|
|
202
|
+
sections.forEach(function(section) {
|
|
203
|
+
if (!section.prs.length) return;
|
|
204
|
+
var details = document.createElement('details');
|
|
205
|
+
details.className = 'tile-section';
|
|
206
|
+
if (section.open) details.open = true;
|
|
207
|
+
var summary = document.createElement('summary');
|
|
208
|
+
summary.className = 'tile-section-summary';
|
|
209
|
+
var label = document.createElement('span');
|
|
210
|
+
label.className = 'tile-section-label';
|
|
211
|
+
label.textContent = section.label;
|
|
212
|
+
var count = document.createElement('span');
|
|
213
|
+
count.className = 'tile-section-count';
|
|
214
|
+
count.textContent = '(' + section.prs.length + ')';
|
|
215
|
+
summary.appendChild(label);
|
|
216
|
+
summary.appendChild(count);
|
|
217
|
+
details.appendChild(summary);
|
|
218
|
+
var sectionBody = document.createElement('div');
|
|
219
|
+
sectionBody.className = 'tile-section-body';
|
|
220
|
+
section.prs.forEach(function(p) { sectionBody.appendChild(prCard(p)); });
|
|
221
|
+
details.appendChild(sectionBody);
|
|
222
|
+
body.appendChild(details);
|
|
182
223
|
});
|
|
183
224
|
}
|
|
184
225
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// chosen, so the indicator never auto-defaults and the user can always clear
|
|
32
32
|
// their choice back to it. Returns the select so the caller can attach a
|
|
33
33
|
// change listener.
|
|
34
|
-
function makeContextSelect(projects, selected) {
|
|
34
|
+
function makeContextSelect(projects, selected, displayByName) {
|
|
35
35
|
var sel = document.createElement('select');
|
|
36
36
|
sel.id = 'chat-context-select';
|
|
37
37
|
sel.setAttribute('aria-label', 'Working in project');
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
var p = projects[i];
|
|
45
45
|
var opt = document.createElement('option');
|
|
46
46
|
opt.value = p;
|
|
47
|
-
opt.textContent = p;
|
|
47
|
+
opt.textContent = (displayByName && displayByName[p]) || p;
|
|
48
48
|
if (p === selected) opt.selected = true;
|
|
49
49
|
sel.appendChild(opt);
|
|
50
50
|
}
|
|
@@ -59,9 +59,11 @@
|
|
|
59
59
|
var res = await fetch('/api/status');
|
|
60
60
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
61
61
|
var data = await res.json();
|
|
62
|
-
var
|
|
63
|
-
.
|
|
64
|
-
|
|
62
|
+
var projectList = (data && Array.isArray(data.projects) ? data.projects : [])
|
|
63
|
+
.filter(function(p) { return p && p.name; });
|
|
64
|
+
var projects = projectList.map(function(p) { return String(p.name); });
|
|
65
|
+
var displayByName = {};
|
|
66
|
+
projectList.forEach(function(p) { displayByName[String(p.name)] = String(p.displayName || p.name); });
|
|
65
67
|
if (projects.length === 0) {
|
|
66
68
|
stripEl.style.display = '';
|
|
67
69
|
controlsEl.replaceChildren(makeContextEmptyNode(), makeContextAddBtn());
|
|
@@ -79,7 +81,7 @@
|
|
|
79
81
|
// (italicized via CSS). We deliberately do NOT fall back to projects[0]
|
|
80
82
|
// here (W-mqayzsj3) so CC turns and project-scoped actions don't
|
|
81
83
|
// silently target the wrong project.
|
|
82
|
-
var sel = makeContextSelect(projects, currentProject);
|
|
84
|
+
var sel = makeContextSelect(projects, currentProject, displayByName);
|
|
83
85
|
stripEl.style.display = '';
|
|
84
86
|
controlsEl.replaceChildren(sel, makeContextAddBtn());
|
|
85
87
|
sel.addEventListener('change', function(ev) {
|
|
@@ -681,6 +681,26 @@
|
|
|
681
681
|
.tile-chip.blue { background: rgba(88, 166, 255, 0.12); color: var(--blue); border-color: var(--blue); }
|
|
682
682
|
.tile-empty { color: var(--muted); font-style: italic; font-size: var(--text-md); }
|
|
683
683
|
|
|
684
|
+
/* Collapsible PR sections (Active / Merged-Completed / Abandoned). Uses
|
|
685
|
+
native <details>/<summary> — no JS wiring needed for the toggle. */
|
|
686
|
+
.tile-section { margin-bottom: 10px; }
|
|
687
|
+
.tile-section:last-child { margin-bottom: 0; }
|
|
688
|
+
.tile-section-summary {
|
|
689
|
+
display: flex; align-items: center; gap: 6px;
|
|
690
|
+
cursor: pointer; user-select: none;
|
|
691
|
+
padding: 4px 2px; margin-bottom: 6px;
|
|
692
|
+
list-style: none;
|
|
693
|
+
font-size: var(--text-base); color: var(--muted);
|
|
694
|
+
}
|
|
695
|
+
.tile-section-summary::-webkit-details-marker { display: none; }
|
|
696
|
+
.tile-section-summary::before {
|
|
697
|
+
content: "▸"; color: var(--muted); font-size: var(--text-sm);
|
|
698
|
+
transition: transform 0.15s ease;
|
|
699
|
+
}
|
|
700
|
+
.tile-section[open] > .tile-section-summary::before { content: "▾"; }
|
|
701
|
+
.tile-section-label { font-weight: 700; letter-spacing: 0.3px; text-transform: uppercase; color: var(--text); }
|
|
702
|
+
.tile-section-count { color: var(--muted); }
|
|
703
|
+
|
|
684
704
|
/* Pinned-context list rows (slim-pinned-modal). */
|
|
685
705
|
.pinned-row {
|
|
686
706
|
border: 1px solid var(--border);
|