@yemi33/minions 0.1.2087 → 0.1.2089
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/dashboard/js/live-stream.js +6 -0
- package/dashboard/js/refresh.js +611 -164
- package/dashboard/js/render-agents.js +2 -0
- package/dashboard/js/render-dispatch.js +94 -6
- package/dashboard/js/render-inbox.js +82 -5
- package/dashboard/js/render-kb.js +9 -4
- package/dashboard/js/render-meetings.js +71 -11
- package/dashboard/js/render-pipelines.js +15 -5
- package/dashboard/js/render-plans.js +14 -9
- package/dashboard/js/render-prd.js +15 -6
- package/dashboard/js/render-prs.js +77 -9
- package/dashboard/js/render-schedules.js +8 -4
- package/dashboard/js/render-utils.js +33 -1
- package/dashboard/js/render-watches.js +23 -3
- package/dashboard/js/render-work-items.js +79 -9
- package/dashboard/js/settings.js +1 -5
- package/dashboard/js/state.js +9 -3
- package/dashboard/js/utils.js +4 -4
- package/dashboard-build.js +1 -1
- package/dashboard.js +400 -358
- package/docs/deprecated.json +0 -13
- package/docs/security.md +23 -0
- package/engine/ado.js +54 -54
- package/engine/cli.js +3 -38
- package/engine/db/index.js +1 -1
- package/engine/db/migrations/002-dispatches.js +1 -1
- package/engine/db/migrations/003-work-items.js +1 -1
- package/engine/db/migrations/004-pull-requests.js +1 -1
- package/engine/dispatch.js +8 -2
- package/engine/github.js +38 -38
- package/engine/lifecycle.js +192 -18
- package/engine/projects.js +92 -0
- package/engine/queries.js +61 -129
- package/engine/shared.js +85 -89
- package/engine/watches.js +5 -5
- package/engine.js +23 -34
- package/package.json +2 -2
|
@@ -160,6 +160,7 @@ function _renderPrdWorkItemIdBadge(workItemId, prdItemId, size, padding) {
|
|
|
160
160
|
function renderPrdProgress(prog) {
|
|
161
161
|
const el = document.getElementById('prd-progress-content');
|
|
162
162
|
const countEl = document.getElementById('prd-progress-count');
|
|
163
|
+
const scrollState = captureDashboardScrollState(el, ['.prd-items-list', '.pl-node-chain']);
|
|
163
164
|
if (!prog) { el.innerHTML = ''; countEl.textContent = '—'; return; }
|
|
164
165
|
const visibleItems = (prog.items || []).filter(i => !isDeleted('plan:' + (i.source || '')) && !isDeleted(_prdItemDeleteKey(i.source, i.id)));
|
|
165
166
|
|
|
@@ -564,8 +565,10 @@ function renderPrdProgress(prog) {
|
|
|
564
565
|
'</div>';
|
|
565
566
|
};
|
|
566
567
|
|
|
567
|
-
// E2E / verification PRs — group by sourcePlan
|
|
568
|
-
|
|
568
|
+
// E2E / verification PRs — group by sourcePlan. Issue #2949 — pullRequests
|
|
569
|
+
// moved off /api/status to /api/pull-requests; refresh.js stashes the fresh
|
|
570
|
+
// fetch into window._lastPullRequests.
|
|
571
|
+
const allPrs = window._lastPullRequests || [];
|
|
569
572
|
const e2eByPlan = {};
|
|
570
573
|
for (const pr of allPrs) {
|
|
571
574
|
// Match by explicit sourcePlan (robust), or fall back to title/branch heuristic (legacy)
|
|
@@ -576,8 +579,11 @@ function renderPrdProgress(prog) {
|
|
|
576
579
|
e2eByPlan[planKey].push(pr);
|
|
577
580
|
}
|
|
578
581
|
|
|
579
|
-
// Find testing guides in prd/ (verify-*.md files)
|
|
580
|
-
|
|
582
|
+
// Find testing guides in prd/ (verify-*.md files). Issue #2949 —
|
|
583
|
+
// verifyGuides moved off /api/status to /api/verify-guides; refresh.js
|
|
584
|
+
// fetches it alongside /api/prd in a Promise.all and stashes onto
|
|
585
|
+
// window._lastVerifyGuides.
|
|
586
|
+
const verifyGuides = window._lastVerifyGuides || [];
|
|
581
587
|
const guideByPlan = {};
|
|
582
588
|
for (const g of verifyGuides) {
|
|
583
589
|
guideByPlan[g.planFile] = g;
|
|
@@ -649,6 +655,7 @@ function renderPrdProgress(prog) {
|
|
|
649
655
|
|
|
650
656
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() or renderMd() by PRD renderers (fields: item ids/names/descriptions, source, projects, PR links, branches, agent labels)
|
|
651
657
|
el.innerHTML = html;
|
|
658
|
+
restoreDashboardScrollState(el, scrollState);
|
|
652
659
|
restoreNotifBadges();
|
|
653
660
|
}
|
|
654
661
|
|
|
@@ -750,9 +757,11 @@ async function prdItemEdit(source, itemId) {
|
|
|
750
757
|
|
|
751
758
|
_prdDescRawCache = item.description || '';
|
|
752
759
|
|
|
753
|
-
// Look up work item and dispatch completion info
|
|
760
|
+
// Look up work item and dispatch completion info. Issue #2949 —
|
|
761
|
+
// dispatch moved off /api/status to /api/dispatch; refresh.js stashes
|
|
762
|
+
// the fresh queue into window._lastDispatch.
|
|
754
763
|
const wi = (window._lastWorkItems || []).find(w => w.id === itemId && w.sourcePlan === source);
|
|
755
|
-
const dispatch = window.
|
|
764
|
+
const dispatch = window._lastDispatch || {};
|
|
756
765
|
const completedEntry = (dispatch.completed || []).find(d =>
|
|
757
766
|
d.meta?.item?.id === itemId && d.meta?.item?.sourcePlan === source
|
|
758
767
|
);
|
|
@@ -4,6 +4,61 @@ let allPrs = [];
|
|
|
4
4
|
let prPage = 0;
|
|
5
5
|
const PR_PER_PAGE = 25;
|
|
6
6
|
|
|
7
|
+
// Fetch each project's pull-requests.json straight off disk through the
|
|
8
|
+
// /state/projects/<name>/pull-requests.json static-file passthrough. This
|
|
9
|
+
// is the issue #2949 escape hatch — the engine writes PR status / review
|
|
10
|
+
// state / build state directly into pull-requests.json, and the mtime-ETag
|
|
11
|
+
// on /state/ guarantees a fresh read on the next 4 s poll regardless of
|
|
12
|
+
// whether the /api/status outer cache has busted.
|
|
13
|
+
//
|
|
14
|
+
// Engine-side enrichment is minimal: stamping `_project` from the project
|
|
15
|
+
// name (the filename context) and back-filling `url` from `prUrlBase` when
|
|
16
|
+
// missing. Both are applied here to keep parity with the legacy slice.
|
|
17
|
+
// Other engine-derived fields (build status from CI poll, reviewStatus,
|
|
18
|
+
// _buildStatusStale) are PERSISTED to disk by the engine's PR pollers, so
|
|
19
|
+
// they survive the round-trip naturally.
|
|
20
|
+
async function fetchPullRequestsFromDisk(projects) {
|
|
21
|
+
if (!Array.isArray(projects) || !projects.length) return null;
|
|
22
|
+
const results = await Promise.all(projects.map(async (p) => {
|
|
23
|
+
const name = p && (p.name || p.path);
|
|
24
|
+
if (!name) return [];
|
|
25
|
+
const urlBase = (p && p.prUrlBase) || '';
|
|
26
|
+
try {
|
|
27
|
+
const r = await fetch('/state/projects/' + encodeURIComponent(name) + '/pull-requests.json');
|
|
28
|
+
if (!r.ok) return [];
|
|
29
|
+
const arr = await r.json();
|
|
30
|
+
if (!Array.isArray(arr)) return [];
|
|
31
|
+
return arr.map((pr) => {
|
|
32
|
+
if (!pr || typeof pr !== 'object' || !pr.id) return null;
|
|
33
|
+
const stamped = Object.assign({ _project: name }, pr);
|
|
34
|
+
if (!stamped.url && urlBase && typeof stamped.number === 'number') {
|
|
35
|
+
stamped.url = urlBase + stamped.number;
|
|
36
|
+
}
|
|
37
|
+
return stamped;
|
|
38
|
+
}).filter(Boolean);
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
const out = [];
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
for (const arr of results) {
|
|
46
|
+
for (const pr of arr) {
|
|
47
|
+
if (seen.has(pr.id)) continue;
|
|
48
|
+
seen.add(pr.id);
|
|
49
|
+
out.push(pr);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Same sort the engine applies: most recent created first, fall back to id.
|
|
53
|
+
out.sort((a, b) => {
|
|
54
|
+
const ac = a.created || '';
|
|
55
|
+
const bc = b.created || '';
|
|
56
|
+
if (ac && bc) return bc.localeCompare(ac);
|
|
57
|
+
return (b.id || '').localeCompare(a.id || '');
|
|
58
|
+
});
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
7
62
|
function _countPrFollowups(pr) {
|
|
8
63
|
// PR follow-up chip (W-mpej3cox00099466) — counts WIs whose
|
|
9
64
|
// meta.pr_followup.parent_pr_url or parent_pr_id matches this PR.
|
|
@@ -152,7 +207,21 @@ function prTableHtml(rows) {
|
|
|
152
207
|
'</tr></thead><tbody>' + rows + '</tbody></table></div>';
|
|
153
208
|
}
|
|
154
209
|
|
|
155
|
-
function
|
|
210
|
+
function _capturePrScrollState(el) {
|
|
211
|
+
const tableWrap = el ? el.querySelector('.pr-table-wrap') : null;
|
|
212
|
+
return tableWrap ? { left: tableWrap.scrollLeft || 0, top: tableWrap.scrollTop || 0 } : null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function _restorePrScrollState(el, state) {
|
|
216
|
+
if (!state) return;
|
|
217
|
+
const tableWrap = el ? el.querySelector('.pr-table-wrap') : null;
|
|
218
|
+
if (!tableWrap) return;
|
|
219
|
+
tableWrap.scrollLeft = state.left || 0;
|
|
220
|
+
tableWrap.scrollTop = state.top || 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderPrs(prs, opts) {
|
|
224
|
+
opts = opts || {};
|
|
156
225
|
prs = prs.filter(p => !isDeleted('pr:' + p.id));
|
|
157
226
|
allPrs = prs;
|
|
158
227
|
const el = document.getElementById('pr-content');
|
|
@@ -163,6 +232,7 @@ function renderPrs(prs) {
|
|
|
163
232
|
return;
|
|
164
233
|
}
|
|
165
234
|
const totalPages = Math.ceil(prs.length / PR_PER_PAGE);
|
|
235
|
+
const previousPage = prPage;
|
|
166
236
|
if (prPage >= totalPages) prPage = totalPages - 1;
|
|
167
237
|
const start = prPage * PR_PER_PAGE;
|
|
168
238
|
const pagePrs = prs.slice(start, start + PR_PER_PAGE);
|
|
@@ -180,18 +250,16 @@ function renderPrs(prs) {
|
|
|
180
250
|
'</div>';
|
|
181
251
|
}
|
|
182
252
|
|
|
183
|
-
const
|
|
184
|
-
|
|
253
|
+
const savedScroll = opts.preserveScroll === false || previousPage !== prPage
|
|
254
|
+
? null
|
|
255
|
+
: _capturePrScrollState(el);
|
|
185
256
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all PR user data wrapped in escapeHtml() by prRow() (fields: PR id, title, description, agent, branch, review/build/status labels)
|
|
186
257
|
el.innerHTML = prTableHtml(rows) + pager;
|
|
187
|
-
|
|
188
|
-
const newWrap = el.querySelector('.pr-table-wrap');
|
|
189
|
-
if (newWrap) newWrap.scrollLeft = savedScroll;
|
|
190
|
-
}
|
|
258
|
+
_restorePrScrollState(el, savedScroll);
|
|
191
259
|
}
|
|
192
260
|
|
|
193
|
-
function prPrev() { if (prPage > 0) { prPage--; renderPrs(allPrs); } }
|
|
194
|
-
function prNext() { const totalPages = Math.ceil(allPrs.length / PR_PER_PAGE); if (prPage < totalPages-1) { prPage++; renderPrs(allPrs); } }
|
|
261
|
+
function prPrev() { if (prPage > 0) { prPage--; renderPrs(allPrs, { preserveScroll: false }); } }
|
|
262
|
+
function prNext() { const totalPages = Math.ceil(allPrs.length / PR_PER_PAGE); if (prPage < totalPages-1) { prPage++; renderPrs(allPrs, { preserveScroll: false }); } }
|
|
195
263
|
|
|
196
264
|
function openAllPrs() {
|
|
197
265
|
const modalEl = document.querySelector('#modal .modal');
|
|
@@ -247,7 +247,7 @@ function _renderViewToggle() {
|
|
|
247
247
|
|
|
248
248
|
function _schedSetView(mode) {
|
|
249
249
|
_schedViewMode = mode;
|
|
250
|
-
renderSchedules(window._lastSchedules || []);
|
|
250
|
+
renderSchedules(window._lastSchedules || [], { preserveScroll: false });
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
function _renderScheduleCalendar(schedules) {
|
|
@@ -310,14 +310,17 @@ function _renderScheduleCalendar(schedules) {
|
|
|
310
310
|
return html;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
function renderSchedules(schedules) {
|
|
313
|
+
function renderSchedules(schedules, opts) {
|
|
314
|
+
opts = opts || {};
|
|
314
315
|
schedules = schedules.filter(function(s) { return !isDeleted('sched:' + s.id); });
|
|
315
316
|
const el = document.getElementById('scheduled-content');
|
|
316
317
|
const countEl = document.getElementById('scheduled-count');
|
|
317
318
|
countEl.textContent = schedules.length;
|
|
318
319
|
window._lastSchedules = schedules;
|
|
320
|
+
const scrollState = opts.preserveScroll === false ? null : captureDashboardScrollState(el, ['.pr-table-wrap']);
|
|
319
321
|
if (!schedules.length) {
|
|
320
322
|
el.innerHTML = '<p class="empty">No scheduled tasks. Add one to automate recurring work.</p>';
|
|
323
|
+
restoreDashboardScrollState(el, scrollState);
|
|
321
324
|
return;
|
|
322
325
|
}
|
|
323
326
|
|
|
@@ -371,10 +374,11 @@ function renderSchedules(schedules) {
|
|
|
371
374
|
|
|
372
375
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() by schedule renderers (fields: schedule id/title/cron/type/project/agent/description)
|
|
373
376
|
el.innerHTML = html;
|
|
377
|
+
restoreDashboardScrollState(el, scrollState);
|
|
374
378
|
}
|
|
375
379
|
|
|
376
|
-
function _schedPrev() { if (_schedPage > 0) { _schedPage--; renderSchedules(window._lastSchedules || []); } }
|
|
377
|
-
function _schedNext() { var tp = Math.ceil((window._lastSchedules || []).length / SCHED_PER_PAGE); if (_schedPage < tp-1) { _schedPage++; renderSchedules(window._lastSchedules || []); } }
|
|
380
|
+
function _schedPrev() { if (_schedPage > 0) { _schedPage--; renderSchedules(window._lastSchedules || [], { preserveScroll: false }); } }
|
|
381
|
+
function _schedNext() { var tp = Math.ceil((window._lastSchedules || []).length / SCHED_PER_PAGE); if (_schedPage < tp-1) { _schedPage++; renderSchedules(window._lastSchedules || [], { preserveScroll: false }); } }
|
|
378
382
|
|
|
379
383
|
function openScheduleDetail(id) {
|
|
380
384
|
const s = (window._lastSchedules || []).find(x => x.id === id);
|
|
@@ -405,4 +405,36 @@ function pinButton(pinKey, pinned, source, opts) {
|
|
|
405
405
|
(pinned ? 'Unpin' : 'Pin') + '</button>';
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
|
|
408
|
+
function _scrollNodesForSelector(root, selector) {
|
|
409
|
+
if (!root || !selector) return [];
|
|
410
|
+
if (selector === ':self') return [root];
|
|
411
|
+
var out = [];
|
|
412
|
+
if (root.matches && root.matches(selector)) out.push(root);
|
|
413
|
+
if (root.querySelectorAll) {
|
|
414
|
+
root.querySelectorAll(selector).forEach(function(el) { out.push(el); });
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function captureDashboardScrollState(root, selectors) {
|
|
420
|
+
var state = [];
|
|
421
|
+
(selectors || []).forEach(function(selector) {
|
|
422
|
+
var nodes = _scrollNodesForSelector(root, selector);
|
|
423
|
+
nodes.forEach(function(el, index) {
|
|
424
|
+
state.push({ selector: selector, index: index, top: el.scrollTop || 0, left: el.scrollLeft || 0 });
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
return state;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function restoreDashboardScrollState(root, state) {
|
|
431
|
+
(state || []).forEach(function(item) {
|
|
432
|
+
var nodes = _scrollNodesForSelector(root, item.selector);
|
|
433
|
+
var el = nodes[item.index];
|
|
434
|
+
if (!el) return;
|
|
435
|
+
el.scrollTop = item.top || 0;
|
|
436
|
+
el.scrollLeft = item.left || 0;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
window.MinionsRenderUtils = { formatToolSummary, renderAgentOutput, renderTerminalBanner, renderPager, pinButton, captureDashboardScrollState, restoreDashboardScrollState };
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
// render-watches.js — Watch rendering and CRUD functions for the dashboard
|
|
2
2
|
|
|
3
|
+
// Pull watches.json straight off disk through /state/engine/watches.json
|
|
4
|
+
// instead of waiting on the /api/status outer cache (issue #2949). The
|
|
5
|
+
// engine writes watch status / _history transitions directly into
|
|
6
|
+
// watches.json, so mtime+size ETag surfaces them on the next 4 s poll.
|
|
7
|
+
// Falls back to data.watches when /state/ returns null.
|
|
8
|
+
async function fetchWatchesFromDisk() {
|
|
9
|
+
try {
|
|
10
|
+
const r = await fetch('/state/engine/watches.json');
|
|
11
|
+
if (!r.ok) return null;
|
|
12
|
+
const arr = await r.json();
|
|
13
|
+
return Array.isArray(arr) ? arr : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
4
20
|
|
|
5
21
|
const _WATCH_STATUS_BADGES = {
|
|
@@ -117,16 +133,19 @@ function _parseIntervalStr(s) {
|
|
|
117
133
|
let _watchPage = 0;
|
|
118
134
|
const WATCH_PER_PAGE = 15;
|
|
119
135
|
|
|
120
|
-
function renderWatches(watchesData) {
|
|
136
|
+
function renderWatches(watchesData, opts) {
|
|
137
|
+
opts = opts || {};
|
|
121
138
|
var watches = (watchesData || []).filter(function(w) { return !isDeleted('watch:' + w.id); });
|
|
122
139
|
var el = document.getElementById('watches-content');
|
|
123
140
|
var countEl = document.getElementById('watches-count');
|
|
124
141
|
if (!el) return;
|
|
125
142
|
if (countEl) countEl.textContent = watches.length;
|
|
126
143
|
window._lastWatches = watches;
|
|
144
|
+
var scrollState = opts.preserveScroll === false ? null : captureDashboardScrollState(el, ['.pr-table-wrap']);
|
|
127
145
|
|
|
128
146
|
if (!watches.length) {
|
|
129
147
|
el.innerHTML = '<p class="empty">No active watches. Create one to monitor PRs or work items.</p>';
|
|
148
|
+
restoreDashboardScrollState(el, scrollState);
|
|
130
149
|
return;
|
|
131
150
|
}
|
|
132
151
|
|
|
@@ -189,10 +208,11 @@ function renderWatches(watchesData) {
|
|
|
189
208
|
|
|
190
209
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: watch id/target/description/action type/owner/timestamps)
|
|
191
210
|
el.innerHTML = html;
|
|
211
|
+
restoreDashboardScrollState(el, scrollState);
|
|
192
212
|
}
|
|
193
213
|
|
|
194
|
-
function _watchPrev() { if (_watchPage > 0) { _watchPage--; renderWatches(window._lastWatches || []); } }
|
|
195
|
-
function _watchNext() { var tp = Math.ceil((window._lastWatches || []).length / WATCH_PER_PAGE); if (_watchPage < tp - 1) { _watchPage++; renderWatches(window._lastWatches || []); } }
|
|
214
|
+
function _watchPrev() { if (_watchPage > 0) { _watchPage--; renderWatches(window._lastWatches || [], { preserveScroll: false }); } }
|
|
215
|
+
function _watchNext() { var tp = Math.ceil((window._lastWatches || []).length / WATCH_PER_PAGE); if (_watchPage < tp - 1) { _watchPage++; renderWatches(window._lastWatches || [], { preserveScroll: false }); } }
|
|
196
216
|
|
|
197
217
|
// ─── Detail Modal ───────────────────────────────────────────────────────────
|
|
198
218
|
|
|
@@ -4,6 +4,76 @@ let allWorkItems = [];
|
|
|
4
4
|
let wiPage = 0;
|
|
5
5
|
const WI_PER_PAGE = 20;
|
|
6
6
|
|
|
7
|
+
// Engine-enrichment field names that live on the /api/status workItems slice
|
|
8
|
+
// but are NOT on disk in projects/<name>/work-items.json. The /state/ fetch
|
|
9
|
+
// recovers raw disk state — these get overlaid from the most recent
|
|
10
|
+
// /api/status snapshot (window._lastWorkItems) so the table still renders the
|
|
11
|
+
// PR chip, pending-reason badge, blockedBy hint, etc. The disk fields win
|
|
12
|
+
// for everything else, so a status flip from `pending` → `done` lands within
|
|
13
|
+
// one /state poll regardless of how stale /api/status got.
|
|
14
|
+
const _WI_ENRICHMENT_FIELDS = [
|
|
15
|
+
'_pr', '_prUrl', '_pendingReason', '_skipReason', '_blockedBy',
|
|
16
|
+
'_humanFeedback', '_reopened', '_managedSpawnPartial', '_securityFlag',
|
|
17
|
+
'_artifacts', 'referencesCount', 'acceptanceCriteriaCount',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Pull each project's work-items.json straight off disk through
|
|
21
|
+
// /state/projects/<name>/work-items.json. The endpoint is a static-file
|
|
22
|
+
// passthrough with mtime+size ETag — there is no server-side cache in the
|
|
23
|
+
// way, so an engine write to `status: 'done'` is visible to the next poll
|
|
24
|
+
// (issue #2949 root fix). Enrichment fields are overlaid from a cached
|
|
25
|
+
// id→item map sourced from window._lastWorkItems (the last /api/status
|
|
26
|
+
// snapshot). All fetches run in parallel; a single project's failure does
|
|
27
|
+
// not block the others.
|
|
28
|
+
async function fetchWorkItemsFromDisk(projects, cachedById) {
|
|
29
|
+
if (!Array.isArray(projects) || !projects.length) return null;
|
|
30
|
+
const results = await Promise.all(projects.map(async (p) => {
|
|
31
|
+
const name = p && (p.name || p.path);
|
|
32
|
+
if (!name) return [];
|
|
33
|
+
try {
|
|
34
|
+
const r = await fetch('/state/projects/' + encodeURIComponent(name) + '/work-items.json');
|
|
35
|
+
if (!r.ok) return [];
|
|
36
|
+
const arr = await r.json();
|
|
37
|
+
if (!Array.isArray(arr)) return [];
|
|
38
|
+
return arr.map((item) => {
|
|
39
|
+
if (!item || typeof item !== 'object') return null;
|
|
40
|
+
const stamped = Object.assign({}, item, { _source: name });
|
|
41
|
+
const cached = cachedById && cachedById.get(item.id);
|
|
42
|
+
if (cached) {
|
|
43
|
+
for (const k of _WI_ENRICHMENT_FIELDS) {
|
|
44
|
+
if (stamped[k] === undefined && cached[k] !== undefined) {
|
|
45
|
+
stamped[k] = cached[k];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// referencesCount / acceptanceCriteriaCount don't live on disk;
|
|
50
|
+
// derive from the raw arrays if present so the table icons render
|
|
51
|
+
// even before the cached snapshot catches up.
|
|
52
|
+
if (stamped.referencesCount == null && Array.isArray(stamped.references)) {
|
|
53
|
+
stamped.referencesCount = stamped.references.length;
|
|
54
|
+
}
|
|
55
|
+
if (stamped.acceptanceCriteriaCount == null && Array.isArray(stamped.acceptanceCriteria)) {
|
|
56
|
+
stamped.acceptanceCriteriaCount = stamped.acceptanceCriteria.length;
|
|
57
|
+
}
|
|
58
|
+
return stamped;
|
|
59
|
+
}).filter(Boolean);
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
// Flatten + dedupe by id (a project rename mid-flight could double-list).
|
|
65
|
+
const out = [];
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
for (const arr of results) {
|
|
68
|
+
for (const item of arr) {
|
|
69
|
+
if (!item.id || seen.has(item.id)) continue;
|
|
70
|
+
seen.add(item.id);
|
|
71
|
+
out.push(item);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
7
77
|
// Track retry state per work item so loading/success/error survives re-renders
|
|
8
78
|
const _wiRetryState = {}; // { [id]: { status: 'pending'|'done'|'error', message?, until? } }
|
|
9
79
|
function setWiRetryState(id, state) { _wiRetryState[id] = state; }
|
|
@@ -107,7 +177,8 @@ function wiRow(item) {
|
|
|
107
177
|
'</tr>';
|
|
108
178
|
}
|
|
109
179
|
|
|
110
|
-
function renderWorkItems(items) {
|
|
180
|
+
function renderWorkItems(items, opts) {
|
|
181
|
+
opts = opts || {};
|
|
111
182
|
items = items.filter(function(w) { return !isDeleted('wi:' + w.id); });
|
|
112
183
|
// Sort: active/dispatched first, then by most recent activity
|
|
113
184
|
const statusOrder = { dispatched: 0, pending: 1, queued: 1, failed: 2, done: 3 };
|
|
@@ -128,6 +199,7 @@ function renderWorkItems(items) {
|
|
|
128
199
|
}
|
|
129
200
|
|
|
130
201
|
const totalPages = Math.ceil(items.length / WI_PER_PAGE);
|
|
202
|
+
const previousPage = wiPage;
|
|
131
203
|
if (wiPage >= totalPages) wiPage = totalPages - 1;
|
|
132
204
|
const start = wiPage * WI_PER_PAGE;
|
|
133
205
|
const pageItems = items.slice(start, start + WI_PER_PAGE);
|
|
@@ -147,14 +219,12 @@ function renderWorkItems(items) {
|
|
|
147
219
|
'</div>';
|
|
148
220
|
}
|
|
149
221
|
|
|
150
|
-
const
|
|
151
|
-
|
|
222
|
+
const scrollState = opts.preserveScroll === false || previousPage !== wiPage
|
|
223
|
+
? null
|
|
224
|
+
: captureDashboardScrollState(el, ['.pr-table-wrap']);
|
|
152
225
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() by wiRow() (fields: work item id/title/source/status/agent/PR links/dates/follow-up metadata)
|
|
153
226
|
el.innerHTML = html;
|
|
154
|
-
|
|
155
|
-
const newWrap = el.querySelector('.pr-table-wrap');
|
|
156
|
-
if (newWrap) newWrap.scrollLeft = savedScroll;
|
|
157
|
-
}
|
|
227
|
+
restoreDashboardScrollState(el, scrollState);
|
|
158
228
|
}
|
|
159
229
|
|
|
160
230
|
async function editWorkItem(id, source) {
|
|
@@ -367,8 +437,8 @@ async function retryWorkItem(id, source) {
|
|
|
367
437
|
}
|
|
368
438
|
}
|
|
369
439
|
|
|
370
|
-
function wiPrev() { if (wiPage > 0) { wiPage--; renderWorkItems(allWorkItems); } }
|
|
371
|
-
function wiNext() { const tp = Math.ceil(allWorkItems.length / WI_PER_PAGE); if (wiPage < tp-1) { wiPage++; renderWorkItems(allWorkItems); } }
|
|
440
|
+
function wiPrev() { if (wiPage > 0) { wiPage--; renderWorkItems(allWorkItems, { preserveScroll: false }); } }
|
|
441
|
+
function wiNext() { const tp = Math.ceil(allWorkItems.length / WI_PER_PAGE); if (wiPage < tp-1) { wiPage++; renderWorkItems(allWorkItems, { preserveScroll: false }); } }
|
|
372
442
|
|
|
373
443
|
let _feedbackRating = null;
|
|
374
444
|
function feedbackWorkItem(id, source) {
|
package/dashboard/js/settings.js
CHANGED
|
@@ -390,11 +390,9 @@ async function openSettings() {
|
|
|
390
390
|
'<div data-search="routing table playbook agent assignment" style="margin-bottom:16px">' +
|
|
391
391
|
'<textarea id="set-routing" rows="10" style="width:100%;padding:8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:monospace;font-size:var(--text-base);resize:vertical">' + escHtml(data.routing || '') + '</textarea>' +
|
|
392
392
|
'</div>' +
|
|
393
|
-
'<h4>Inbox
|
|
393
|
+
'<h4>Inbox</h4>' +
|
|
394
394
|
'<div class="settings-grid-2">' +
|
|
395
395
|
settingsField('Consolidation Threshold', 'set-inboxConsolidateThreshold', e.inboxConsolidateThreshold || 5, 'notes', 'Inbox notes before auto-consolidation') +
|
|
396
|
-
settingsField('Status WorkItems Retention', 'set-statusWorkItemsRetentionDays', e.statusWorkItemsRetentionDays ?? 0, 'days', 'Optional age-based trim for done/failed/cancelled work items in the /api/status workItems slice (active items are always shipped). Default 0 = no trim — the slim projection that strips description/AC/references already cuts the payload by ~80%. Set to a positive integer to also drop terminal items older than N days.') +
|
|
397
|
-
settingsField('Status Meetings Retention', 'set-statusMeetingsRetentionDays', e.statusMeetingsRetentionDays ?? 0, 'days', 'Optional age-based trim for completed/archived meetings in the /api/status meetings slice (active meetings are always shipped). Default 0 = no trim — the slim projection that collapses findings/debate/transcript bodies to {agentId: true} sentinels already cuts the payload by ~80%. Set to a positive integer to also drop terminal meetings older than N days. Detail modal still fetches full bodies via /api/meetings/:id.') +
|
|
398
396
|
settingsField('Version Check Interval', 'set-versionCheckInterval', e.versionCheckInterval || 3600000, 'ms', 'How often to check npm for updates (default: 1 hour)') +
|
|
399
397
|
'</div>' +
|
|
400
398
|
'<h4>Operator & Comments</h4>' +
|
|
@@ -841,8 +839,6 @@ async function saveSettings() {
|
|
|
841
839
|
restartGracePeriod: document.getElementById('set-restartGracePeriod').value,
|
|
842
840
|
meetingRoundTimeout: document.getElementById('set-meetingRoundTimeout').value,
|
|
843
841
|
operatorLogin: (document.getElementById('set-operatorLogin')?.value ?? '').trim(),
|
|
844
|
-
statusWorkItemsRetentionDays: document.getElementById('set-statusWorkItemsRetentionDays').value,
|
|
845
|
-
statusMeetingsRetentionDays: document.getElementById('set-statusMeetingsRetentionDays').value,
|
|
846
842
|
autoApprovePlans: document.getElementById('set-autoApprovePlans').checked,
|
|
847
843
|
evalLoop: document.getElementById('set-evalLoop').checked,
|
|
848
844
|
autoDecompose: document.getElementById('set-autoDecompose').checked,
|
package/dashboard/js/state.js
CHANGED
|
@@ -208,9 +208,15 @@ document.addEventListener('visibilitychange', function() {
|
|
|
208
208
|
});
|
|
209
209
|
|
|
210
210
|
function rerenderPrdFromCache() {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
// Issue #2949 — prdProgress + prd moved off /api/status to /api/prd;
|
|
212
|
+
// refresh.js stashes the fresh fetch into window._lastPrdProgress +
|
|
213
|
+
// window._lastPrdStatus. `!== undefined` (not a truthy check) so a
|
|
214
|
+
// cache-was-null state (last PRD archived → /api/prd returned
|
|
215
|
+
// {progress: null}) still triggers a re-render — renderPrd handles
|
|
216
|
+
// null gracefully and paints the empty 'No PRD found' affordance.
|
|
217
|
+
if (window._lastPrdProgress === undefined) return;
|
|
218
|
+
renderPrdProgress(window._lastPrdProgress);
|
|
219
|
+
renderPrd(window._lastPrdStatus, window._lastPrdProgress);
|
|
214
220
|
}
|
|
215
221
|
|
|
216
222
|
// Global fetch wrapper with timeout — prevents connection exhaustion from hung requests.
|
package/dashboard/js/utils.js
CHANGED
|
@@ -53,8 +53,8 @@ function kbPinKey(cat, file) { return 'knowledge/' + cat + '/' + file; }
|
|
|
53
53
|
function _togglePinAndRefresh(key, source) {
|
|
54
54
|
var pinned = togglePin(key);
|
|
55
55
|
showToast('cmd-toast', pinned ? 'Pinned to top' : 'Unpinned', true);
|
|
56
|
-
if (source === 'inbox') renderInbox(inboxData);
|
|
57
|
-
else if (source === 'kb') renderKnowledgeBase();
|
|
56
|
+
if (source === 'inbox') renderInbox(inboxData, { preserveScroll: false });
|
|
57
|
+
else if (source === 'kb') renderKnowledgeBase({ preserveScroll: false });
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// Modal navigation stack — enables back button when opening a modal from another modal
|
|
@@ -93,8 +93,8 @@ function toggleModalPin() {
|
|
|
93
93
|
var pinned = togglePin(_modalFilePath);
|
|
94
94
|
showToast('cmd-toast', pinned ? 'Pinned to top' : 'Unpinned', true);
|
|
95
95
|
updateModalPinBtn();
|
|
96
|
-
if (_modalFilePath.startsWith('notes/inbox/')) renderInbox(inboxData);
|
|
97
|
-
else if (_modalFilePath.startsWith('knowledge/')) renderKnowledgeBase();
|
|
96
|
+
if (_modalFilePath.startsWith('notes/inbox/')) renderInbox(inboxData, { preserveScroll: false });
|
|
97
|
+
else if (_modalFilePath.startsWith('knowledge/')) renderKnowledgeBase({ preserveScroll: false });
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Canonical HTML-escape helper (SEC-03). Use this in all new code and for any user-controlled
|
package/dashboard-build.js
CHANGED
|
@@ -29,7 +29,7 @@ function buildDashboardHtml() {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const jsFiles = [
|
|
32
|
-
'utils', 'state', 'features-client', 'detail-panel', 'live-stream',
|
|
32
|
+
'utils', 'state', 'features-client', 'render-utils', 'detail-panel', 'live-stream',
|
|
33
33
|
'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
|
|
34
34
|
'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
|
|
35
35
|
'render-other', 'render-managed', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
|