@yemi33/minions 0.1.2177 → 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 +9 -1
- package/dashboard/js/render-dispatch.js +92 -0
- package/dashboard/js/render-other.js +1 -1
- package/dashboard/js/render-plans.js +82 -13
- 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/js/settings.js +100 -11
- package/dashboard/layout.html +6 -0
- 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/styles.css +39 -0
- package/dashboard-build.js +17 -2
- package/dashboard.js +469 -21
- package/docs/README.md +8 -1
- package/docs/auto-discovery.md +40 -0
- package/docs/branch-derivation.md +13 -1
- package/docs/cross-repo-plans.md +292 -0
- package/docs/deprecated.json +4 -4
- package/docs/pr-auto-fix-dispatch.md +64 -0
- package/docs/pr-review-fix-loop.md +1 -1
- package/docs/watches.md +1 -0
- package/engine/ado.js +1 -10
- package/engine/diagnostics-memory.js +190 -0
- package/engine/dispatch.js +53 -0
- package/engine/lifecycle.js +155 -191
- package/engine/meeting.js +30 -0
- package/engine/playbook.js +15 -0
- package/engine/queries.js +165 -5
- package/engine/runtimes/copilot.js +19 -0
- package/engine/shared.js +303 -3
- package/engine/watchdog.js +6 -0
- package/engine.js +576 -113
- package/package.json +2 -2
- package/playbooks/plan-to-prd.md +25 -2
- package/playbooks/plan.md +4 -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
|
@@ -134,7 +134,7 @@ const RENDER_VERSIONS = {
|
|
|
134
134
|
prdProgress: 1,
|
|
135
135
|
prdPrs: 1,
|
|
136
136
|
inbox: 2,
|
|
137
|
-
projects:
|
|
137
|
+
projects: 2,
|
|
138
138
|
notes: 1,
|
|
139
139
|
prd: 1,
|
|
140
140
|
prs: 3,
|
|
@@ -143,6 +143,7 @@ const RENDER_VERSIONS = {
|
|
|
143
143
|
version: 1,
|
|
144
144
|
adoThrottle: 1,
|
|
145
145
|
ghThrottle: 1,
|
|
146
|
+
pausedBanner: 1,
|
|
146
147
|
dispatch: 2,
|
|
147
148
|
engineLog: 2,
|
|
148
149
|
metrics: 1,
|
|
@@ -713,6 +714,13 @@ function _processStatusUpdate(data, opts) {
|
|
|
713
714
|
_safeRender('adoThrottle', function() { renderAdoThrottleAlert(data.adoThrottle); });
|
|
714
715
|
_changed('ghThrottle', data.ghThrottle);
|
|
715
716
|
_safeRender('ghThrottle', function() { renderGhThrottleAlert(data.ghThrottle); });
|
|
717
|
+
// P-g7a2b4c5 — sticky kill-switch banner. Cache key is the pair of paused
|
|
718
|
+
// flags so the banner only re-renders when either flag actually flips (the
|
|
719
|
+
// full data.engine slice changes every tick because heartbeat/lastTickAt
|
|
720
|
+
// advance).
|
|
721
|
+
const _pausedCacheKey = (data.engine && (data.engine.pollingPaused ? 'p' : '-') + (data.engine.autoFixPaused ? 'a' : '-')) || '--';
|
|
722
|
+
_changed('pausedBanner', _pausedCacheKey);
|
|
723
|
+
_safeRender('pausedBanner', function() { renderPausedBanner(data.engine); });
|
|
716
724
|
// Dispatch now comes from /api/dispatch — a dedicated fresh-JSON
|
|
717
725
|
// endpoint that re-runs getDispatchQueue() server-side on every
|
|
718
726
|
// request (issue #2949). Completion-report sidecars are now loaded
|
|
@@ -245,6 +245,98 @@ function renderGhThrottleAlert(ghThrottle) {
|
|
|
245
245
|
el.style.display = 'flex';
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// P-g7a2b4c5 — Sticky cross-page kill-switch banner. Renders whenever either
|
|
249
|
+
// engine.pollingPaused or engine.autoFixPaused is true, with per-row Resume
|
|
250
|
+
// buttons that POST to the convenience endpoints from P-f3c9d0e7. Built with
|
|
251
|
+
// the DOM API (no innerHTML) so the eslint-plugin-no-unsanitized gate passes
|
|
252
|
+
// cleanly — banner content is system-controlled but follows the project
|
|
253
|
+
// convention of avoiding innerHTML in new code anyway.
|
|
254
|
+
function renderPausedBanner(engine) {
|
|
255
|
+
const el = document.getElementById('paused-banner');
|
|
256
|
+
if (!el) return;
|
|
257
|
+
const pollingPaused = !!(engine && engine.pollingPaused);
|
|
258
|
+
const autoFixPaused = !!(engine && engine.autoFixPaused);
|
|
259
|
+
if (!pollingPaused && !autoFixPaused) {
|
|
260
|
+
el.hidden = true;
|
|
261
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Wipe + rebuild — list-of-pauses model, one row per active pause.
|
|
266
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
267
|
+
el.hidden = false;
|
|
268
|
+
|
|
269
|
+
const title = document.createElement('div');
|
|
270
|
+
title.className = 'paused-banner-title';
|
|
271
|
+
title.textContent = '⚠️ Some engine functions are paused:';
|
|
272
|
+
el.appendChild(title);
|
|
273
|
+
|
|
274
|
+
const list = document.createElement('ul');
|
|
275
|
+
list.className = 'paused-banner-list';
|
|
276
|
+
el.appendChild(list);
|
|
277
|
+
|
|
278
|
+
const rows = [];
|
|
279
|
+
if (pollingPaused) {
|
|
280
|
+
rows.push({
|
|
281
|
+
key: 'polling',
|
|
282
|
+
label: 'Polling',
|
|
283
|
+
detail: 'no new PR state arriving',
|
|
284
|
+
endpoint: '/api/engine/polling/resume',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (autoFixPaused) {
|
|
288
|
+
rows.push({
|
|
289
|
+
key: 'autofix',
|
|
290
|
+
label: 'PR auto-fix',
|
|
291
|
+
detail: 'no new PR-triggered dispatches',
|
|
292
|
+
endpoint: '/api/engine/auto-fix/resume',
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
rows.forEach(function (row) {
|
|
297
|
+
const li = document.createElement('li');
|
|
298
|
+
li.className = 'paused-banner-row';
|
|
299
|
+
li.setAttribute('data-paused-row', row.key);
|
|
300
|
+
|
|
301
|
+
const labelSpan = document.createElement('span');
|
|
302
|
+
labelSpan.className = 'paused-banner-row-label';
|
|
303
|
+
labelSpan.textContent = '• ' + row.label;
|
|
304
|
+
li.appendChild(labelSpan);
|
|
305
|
+
|
|
306
|
+
const detailSpan = document.createElement('span');
|
|
307
|
+
detailSpan.className = 'paused-banner-row-detail';
|
|
308
|
+
detailSpan.textContent = '— ' + row.detail;
|
|
309
|
+
li.appendChild(detailSpan);
|
|
310
|
+
|
|
311
|
+
const btn = document.createElement('button');
|
|
312
|
+
btn.type = 'button';
|
|
313
|
+
btn.className = 'paused-banner-row-resume';
|
|
314
|
+
btn.textContent = 'Resume';
|
|
315
|
+
btn.setAttribute('data-paused-resume', row.key);
|
|
316
|
+
btn.addEventListener('click', function () {
|
|
317
|
+
if (btn.disabled) return;
|
|
318
|
+
btn.disabled = true;
|
|
319
|
+
btn.classList.add('clicked');
|
|
320
|
+
fetch(row.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' } })
|
|
321
|
+
.then(function (resp) {
|
|
322
|
+
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
323
|
+
if (typeof showToast === 'function') showToast('cmd-toast', row.label + ' resumed', true);
|
|
324
|
+
if (typeof refreshNow === 'function') refreshNow();
|
|
325
|
+
})
|
|
326
|
+
.catch(function (err) {
|
|
327
|
+
btn.disabled = false;
|
|
328
|
+
btn.classList.remove('clicked');
|
|
329
|
+
if (typeof showToast === 'function') {
|
|
330
|
+
showToast('cmd-toast', 'Resume failed: ' + (err && err.message ? err.message : 'unknown'), false);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
li.appendChild(btn);
|
|
335
|
+
|
|
336
|
+
list.appendChild(li);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
248
340
|
function renderDispatch(dispatch, opts) {
|
|
249
341
|
opts = opts || {};
|
|
250
342
|
if (!dispatch) return;
|
|
@@ -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>'
|