@yemi33/minions 0.1.2086 → 0.1.2088
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/refresh.js +598 -160
- package/dashboard/js/render-dispatch.js +77 -0
- package/dashboard/js/render-inbox.js +72 -0
- package/dashboard/js/render-meetings.js +55 -0
- package/dashboard/js/render-plans.js +14 -9
- package/dashboard/js/render-prd.js +13 -6
- package/dashboard/js/render-prs.js +55 -0
- package/dashboard/js/render-watches.js +16 -0
- package/dashboard/js/render-work-items.js +70 -0
- package/dashboard/js/settings.js +1 -5
- package/dashboard/js/state.js +9 -3
- package/dashboard.js +557 -351
- 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
package/dashboard/js/refresh.js
CHANGED
|
@@ -2,43 +2,109 @@
|
|
|
2
2
|
|
|
3
3
|
// Sidebar activity indicators — detect changes between refreshes
|
|
4
4
|
// Registry: add one line per page. Counter returns a value; badge shows when value increases.
|
|
5
|
+
// Sidebar activity-dot signatures. Each function returns a STRING (sig
|
|
6
|
+
// computed from the page's underlying state) OR `null` when the cache
|
|
7
|
+
// isn't populated yet — null suppresses the comparison so a slice going
|
|
8
|
+
// from undefined → populated does NOT flip the dot on the first tick
|
|
9
|
+
// after a fetch resolves. Once a signature is computable, it's compared
|
|
10
|
+
// against the previously-stored one; any change fires the dot.
|
|
11
|
+
//
|
|
12
|
+
// All migrated slices read from window._lastX globals set inside async
|
|
13
|
+
// /api/<x> .then handlers (issue #2949). Returning null while those
|
|
14
|
+
// globals are still undefined prevents the timing race where _detectPage-
|
|
15
|
+
// Changes runs synchronously at the end of _processStatusUpdate before
|
|
16
|
+
// the .then handlers have populated the caches.
|
|
17
|
+
//
|
|
18
|
+
// dispatch.completed is capped at 20 server-side, so length-based counters
|
|
19
|
+
// peg once the cap is hit; we fingerprint the LAST completed entry's id +
|
|
20
|
+
// completed_at instead so a new completion rotates the signature whether
|
|
21
|
+
// the cap is reached or not.
|
|
5
22
|
const _pageCounters = {
|
|
6
|
-
home:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
home: function(d) {
|
|
24
|
+
// dispatch is on /api/dispatch — wait for the cache.
|
|
25
|
+
const dispatch = window._lastDispatch || d.dispatch;
|
|
26
|
+
if (!dispatch) return null;
|
|
27
|
+
const completed = dispatch.completed || [];
|
|
28
|
+
const last = completed[completed.length - 1];
|
|
29
|
+
return completed.length + '|' + (last?.id || '') + '|' + (last?.completed_at || '');
|
|
30
|
+
},
|
|
31
|
+
work: function(d) {
|
|
32
|
+
if (!Array.isArray(window._lastWorkItems) && !Array.isArray(d.workItems)) return null;
|
|
33
|
+
const wis = window._lastWorkItems || d.workItems || [];
|
|
34
|
+
return wis.length + '|' + wis.filter(function(w) { return w.status === 'pending' || w.status === 'dispatched'; }).length;
|
|
35
|
+
},
|
|
36
|
+
plans: function(d) {
|
|
37
|
+
// /api/plans → window._lastPlans (refreshPlans()); /api/prd →
|
|
38
|
+
// window._lastPrdProgress. Both must be populated before we sig.
|
|
39
|
+
if (!Array.isArray(window._lastPlans) || window._lastPrdProgress === undefined) return null;
|
|
40
|
+
const prog = window._lastPrdProgress || {};
|
|
41
|
+
const plans = window._lastPlans;
|
|
42
|
+
return (prog.complete || 0) + '|' + plans.length + '|' + plans.map(function(p) { return (p.file || '') + ':' + (p.status || ''); }).join(',');
|
|
43
|
+
},
|
|
44
|
+
prs: function(d) {
|
|
45
|
+
if (!Array.isArray(window._lastPullRequests) && !Array.isArray(d.pullRequests)) return null;
|
|
46
|
+
const prs = window._lastPullRequests || d.pullRequests || [];
|
|
47
|
+
const counts = {};
|
|
48
|
+
for (const p of prs) counts[p.status || ''] = (counts[p.status || ''] || 0) + 1;
|
|
49
|
+
return prs.length + '|' + Object.keys(counts).sort().map(function(k) { return k + ':' + counts[k]; }).join(',');
|
|
50
|
+
},
|
|
51
|
+
inbox: function(d) {
|
|
52
|
+
// Wait for BOTH inbox and notes to populate — either alone gives a
|
|
53
|
+
// half-stale sig on first arrival.
|
|
54
|
+
if (!Array.isArray(window._lastInbox) || window._lastNotes === undefined) return null;
|
|
55
|
+
const ib = window._lastInbox;
|
|
56
|
+
const notes = window._lastNotes || {};
|
|
57
|
+
return ib.length + '|' + (notes.content || '').length;
|
|
58
|
+
},
|
|
59
|
+
watches: function(d) {
|
|
60
|
+
if (!Array.isArray(window._lastWatches) && !Array.isArray(d.watches)) return null;
|
|
61
|
+
const ws = window._lastWatches || d.watches || [];
|
|
62
|
+
return ws.length + '|' + ws.reduce(function(m, w) { return Math.max(m, new Date(w.last_triggered || 0).getTime() || 0); }, 0);
|
|
63
|
+
},
|
|
64
|
+
meetings: function(d) {
|
|
65
|
+
// Meetings total comes from /api/meetings-total; list from /state/meetings.
|
|
66
|
+
if (window._lastMeetingsTotal === undefined && d.meetingsTotal === undefined) return null;
|
|
67
|
+
const total = window._lastMeetingsTotal ?? d.meetingsTotal ?? 0;
|
|
68
|
+
const list = window._lastMeetings || d.meetings || [];
|
|
69
|
+
return total + '|' + list.reduce(function(s, m) { return s + (m.round || 0); }, 0);
|
|
70
|
+
},
|
|
71
|
+
pipelines: function(d) {
|
|
72
|
+
if (!Array.isArray(window._lastPipelines) && !Array.isArray(d.pipelines)) return null;
|
|
73
|
+
const pls = window._lastPipelines || d.pipelines || [];
|
|
74
|
+
return pls.length + '|' + pls.reduce(function(s, p) { return s + (p.runs || []).length; }, 0);
|
|
75
|
+
},
|
|
76
|
+
schedule: function(d) {
|
|
77
|
+
if (!Array.isArray(window._lastSchedules) && !Array.isArray(d.schedules)) return null;
|
|
78
|
+
const sch = window._lastSchedules || d.schedules || [];
|
|
79
|
+
return sch.length + '|' + sch.map(function(s) { return (s.id || '') + ':' + (s.enabled === false ? '0' : '1') + ':' + (s.cron || ''); }).sort().join(',');
|
|
80
|
+
},
|
|
81
|
+
tools: function(d) {
|
|
82
|
+
// skills + mcpServers are on /api/status directly, no cache to wait for.
|
|
83
|
+
return (d.skills || []).length + '|' + (d.mcpServers || []).length;
|
|
84
|
+
},
|
|
85
|
+
engine: function(d) {
|
|
86
|
+
const dispatch = window._lastDispatch || d.dispatch;
|
|
87
|
+
if (!dispatch) return null;
|
|
88
|
+
const errors = (dispatch.completed || []).filter(function(c) { return c.result === 'error'; });
|
|
89
|
+
const lastErr = errors[errors.length - 1];
|
|
90
|
+
return errors.length + '|' + (lastErr?.id || '') + '|' + (lastErr?.completed_at || '');
|
|
91
|
+
},
|
|
92
|
+
qa: function(d) {
|
|
93
|
+
if (!window._lastQaRunsSummary && !d.qaRuns) return null;
|
|
94
|
+
const q = window._lastQaRunsSummary || d.qaRuns || {};
|
|
95
|
+
return (q.total || 0) + '|' + (q.sig || '');
|
|
96
|
+
},
|
|
32
97
|
};
|
|
33
98
|
let _prevCounts = {};
|
|
34
99
|
function _detectPageChanges(data) {
|
|
35
100
|
var changes = {};
|
|
36
|
-
var counts = {};
|
|
37
101
|
for (var page in _pageCounters) {
|
|
38
|
-
|
|
39
|
-
if (
|
|
102
|
+
var sig = _pageCounters[page](data);
|
|
103
|
+
if (sig === null) continue; // cache not ready yet — skip both compare and record
|
|
104
|
+
var sigStr = String(sig);
|
|
105
|
+
if (_prevCounts[page] !== undefined && sigStr !== _prevCounts[page]) changes[page] = true;
|
|
106
|
+
_prevCounts[page] = sigStr;
|
|
40
107
|
}
|
|
41
|
-
_prevCounts = counts;
|
|
42
108
|
return changes;
|
|
43
109
|
}
|
|
44
110
|
|
|
@@ -246,23 +312,99 @@ function _startNextTickTicker() {
|
|
|
246
312
|
_engineNextTickTimer = setInterval(_updateNextTickChip, 1000);
|
|
247
313
|
}
|
|
248
314
|
|
|
315
|
+
// Background-poll the sidebar-counter summaries from their dedicated
|
|
316
|
+
// endpoints (issue #2949). Results land on window._last* globals which
|
|
317
|
+
// _pageCounters reads with a /api/status field fallback. Fire-and-forget
|
|
318
|
+
// — no awaits — so this never blocks the synchronous render chain. The
|
|
319
|
+
// fetched values arrive ~1 tick late on the FIRST refresh after page load;
|
|
320
|
+
// subsequent ticks see fresh signatures from the dedicated endpoints with
|
|
321
|
+
// no /api/status outer-cache staleness.
|
|
322
|
+
// Fire the F1/F3 cross-slice triggers when work-items / pull-requests /
|
|
323
|
+
// dispatch resolves with NEW data on this tick. Cross-slice dependency graph:
|
|
324
|
+
// workItems → {prs (+N follow-up chip), plans (derivePlanStatus)}
|
|
325
|
+
// pullRequests → {plans (derivePlanStatus)}
|
|
326
|
+
// dispatch → {plans (derivePlanStatus reads .active/.pending)} (Round-3 #5.)
|
|
327
|
+
//
|
|
328
|
+
// Signature is a CHEAP fingerprint derived from key fields, NOT a full
|
|
329
|
+
// JSON.stringify — the latter allocated multi-MB strings on every tick
|
|
330
|
+
// for the typical WI/PR payloads and contradicted the dashboard event-loop
|
|
331
|
+
// budget. Catch-fallback uses a STABLE token (not Date.now()) so a payload
|
|
332
|
+
// whose getter throws doesn't perpetually re-fire. (Round-4 #1 + #3.)
|
|
333
|
+
const _crossSliceSig = { workItems: '', pullRequests: '', dispatch: '' };
|
|
334
|
+
function _cheapSig(value) {
|
|
335
|
+
try {
|
|
336
|
+
if (!value) return 'null';
|
|
337
|
+
if (Array.isArray(value)) {
|
|
338
|
+
const head = value[0] || {};
|
|
339
|
+
const tail = value[value.length - 1] || {};
|
|
340
|
+
return value.length + '|' + (head.id || '') + ':' + (head.status || head.state || '') + '|' + (tail.id || '') + ':' + (tail.status || tail.state || head.updatedAt || '');
|
|
341
|
+
}
|
|
342
|
+
if (typeof value === 'object') {
|
|
343
|
+
// Dispatch shape: {pending, active, completed} — fingerprint the lengths
|
|
344
|
+
// + first id of each so any insertion/removal advances the sig.
|
|
345
|
+
const p = Array.isArray(value.pending) ? value.pending : [];
|
|
346
|
+
const a = Array.isArray(value.active) ? value.active : [];
|
|
347
|
+
const c = Array.isArray(value.completed) ? value.completed : [];
|
|
348
|
+
return 'd:' + p.length + ':' + (p[0]?.id || '') + '/' + a.length + ':' + (a[0]?.id || '') + '/' + c.length + ':' + (c[c.length - 1]?.id || '');
|
|
349
|
+
}
|
|
350
|
+
return String(value);
|
|
351
|
+
} catch { return '__err__'; }
|
|
352
|
+
}
|
|
353
|
+
// F3 coalescer: batch multiple advancing-key calls into ONE renderPlans per
|
|
354
|
+
// tick. Without this, a tick where all three of workItems/pullRequests/
|
|
355
|
+
// dispatch advance would re-render plans 3-4 times back-to-back for
|
|
356
|
+
// identical output. (Round-4 #2.)
|
|
357
|
+
let _planRerenderQueued = false;
|
|
358
|
+
function _queuePlanRerender() {
|
|
359
|
+
if (_planRerenderQueued) return;
|
|
360
|
+
_planRerenderQueued = true;
|
|
361
|
+
Promise.resolve().then(function () {
|
|
362
|
+
_planRerenderQueued = false;
|
|
363
|
+
if (Array.isArray(window._lastPlans) && typeof renderPlans === 'function') {
|
|
364
|
+
_safeRender('plans:cross-slice', function() { renderPlans(window._lastPlans); });
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
function _fireCrossSliceRender(key, value) {
|
|
369
|
+
const sig = _cheapSig(value);
|
|
370
|
+
if (_crossSliceSig[key] === sig) return false;
|
|
371
|
+
_crossSliceSig[key] = sig;
|
|
372
|
+
// F1 — re-render PRs (+N follow-up chip count depends on the WI list).
|
|
373
|
+
// Fires for both workItems and pullRequests keys so a late PR fetch
|
|
374
|
+
// refreshes the +N chip against the cached WI list. (Round-3 #7.)
|
|
375
|
+
if ((key === 'workItems' || key === 'pullRequests') && Array.isArray(window._lastPullRequests)) {
|
|
376
|
+
_safeRender('prs:cross-slice', function() { renderPrs(window._lastPullRequests); });
|
|
377
|
+
}
|
|
378
|
+
// F3 — coalesced via microtask so 3 advancing keys → 1 plans render.
|
|
379
|
+
_queuePlanRerender();
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function _refreshSidebarCounterSummaries() {
|
|
384
|
+
fetch('/api/qa-runs-summary')
|
|
385
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
386
|
+
.then(function (s) { if (s && typeof s === 'object') window._lastQaRunsSummary = s; })
|
|
387
|
+
.catch(function () { /* sidebar counter degrades to /api/status fallback */ });
|
|
388
|
+
fetch('/api/meetings-total')
|
|
389
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
390
|
+
.then(function (s) { if (s && typeof s.total === 'number') window._lastMeetingsTotal = s.total; })
|
|
391
|
+
.catch(function () { /* optional */ });
|
|
392
|
+
// /api/verify-guides is now fetched in parallel with /api/prd inside the
|
|
393
|
+
// prdProgress block so renderPrd's guideByPlan lookup uses the SAME tick's
|
|
394
|
+
// guide list as the PRD progress data. (Round-2 review finding #5.)
|
|
395
|
+
// QA sessions summary was previously fetched here, but no _pageCounters
|
|
396
|
+
// entry read it — drop the dead endpoint + fetch. (Review finding #9.)
|
|
397
|
+
}
|
|
398
|
+
|
|
249
399
|
function _processStatusUpdate(data) {
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
//
|
|
400
|
+
// Kick off the dedicated-endpoint summary refreshes (issue #2949). They
|
|
401
|
+
// run in the background and update window._last* globals that the sidebar
|
|
402
|
+
// _pageCounters read. No await — the current tick continues immediately.
|
|
403
|
+
_refreshSidebarCounterSummaries();
|
|
404
|
+
// Fresh install id tracking — kept ONLY to update localStorage so the
|
|
405
|
+
// value is available to other code that introspects it; the tab does
|
|
406
|
+
// NOT auto-reload on a MINIONS_HOME swap. User decides when to refresh.
|
|
255
407
|
if (data.installId) {
|
|
256
|
-
const prev = localStorage.getItem('minions-install-id');
|
|
257
|
-
if (prev && prev !== data.installId) {
|
|
258
|
-
localStorage.clear();
|
|
259
|
-
// Write the new install-id BEFORE reload — the post-reload poll must see
|
|
260
|
-
// a matching id so it stops looping. clear() above wiped it, so set it again.
|
|
261
|
-
localStorage.setItem('minions-install-id', data.installId);
|
|
262
|
-
console.log('Minions: fresh install detected, reloading to reset module caches');
|
|
263
|
-
location.reload();
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
408
|
localStorage.setItem('minions-install-id', data.installId);
|
|
267
409
|
}
|
|
268
410
|
// Always update cheap elements
|
|
@@ -305,8 +447,12 @@ function _processStatusUpdate(data) {
|
|
|
305
447
|
// refactor that adds a window._last* read to those renderers will see
|
|
306
448
|
// fresh data on the same tick, not the previous one. Covered by
|
|
307
449
|
// dashboard-resilience.test.js source-inspection assertions.
|
|
308
|
-
window._lastDispatch
|
|
309
|
-
|
|
450
|
+
// window._lastDispatch + window._lastWorkItems are now exclusively set
|
|
451
|
+
// inside the /api/dispatch + /api/work-items .then handlers (issue #2949).
|
|
452
|
+
// The hoist here used to read data.dispatch / data.workItems from the
|
|
453
|
+
// /api/status slim slices — both undefined post-slim — and overwrite the
|
|
454
|
+
// freshly-fetched values from the prior tick with undefined/[], which
|
|
455
|
+
// caused first-tick sidebar-dot flashes on every migrated page.
|
|
310
456
|
window._lastStatus = data;
|
|
311
457
|
|
|
312
458
|
|
|
@@ -318,24 +464,113 @@ function _processStatusUpdate(data) {
|
|
|
318
464
|
// cycled out of the slim /api/status payload. Per-call try/catch keeps
|
|
319
465
|
// each renderer independent; throws log to Console (no UI banner).
|
|
320
466
|
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
467
|
+
// The per-slice _changed(key, data[key]) calls were dropped for every
|
|
468
|
+
// migrated slice (issue #2949) — data[key] is undefined post-slim, so
|
|
469
|
+
// every call fed a permanently-stable 'undefined' signature into the
|
|
470
|
+
// section cache that no consumer reads. The F1/F3 cross-slice triggers
|
|
471
|
+
// that previously gated on _workItemsChanged / _prsChanged moved into
|
|
472
|
+
// the /api/work-items + /api/pull-requests .then handlers
|
|
473
|
+
// (_fireCrossSliceRender) where the FRESH fetched lists are available.
|
|
474
|
+
// Agents now come from /api/agents — a dedicated fresh-JSON endpoint with
|
|
475
|
+
// input-mtime ETag (issue #2949). No /api/status outer cache in the path.
|
|
476
|
+
// cmdUpdateAgentList consumes the same slice but renders the command-line
|
|
477
|
+
// chip pop-up so we feed it the fresh array too.
|
|
478
|
+
_safeRender('agents', function() {
|
|
479
|
+
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
480
|
+
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
481
|
+
window._lastRequestedSeq.agents = seq;
|
|
482
|
+
fetch('/api/agents')
|
|
483
|
+
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
484
|
+
.then(function (fresh) {
|
|
485
|
+
if (seq < (window._lastRequestedSeq.agents || 0)) return;
|
|
486
|
+
const list = Array.isArray(fresh) ? fresh : window._lastAgents;
|
|
487
|
+
if (list) window._lastAgents = list;
|
|
488
|
+
_safeRender('agents', function() { renderAgents(list); });
|
|
489
|
+
_safeRender('cmdUpdateAgentList', function() { cmdUpdateAgentList(list); });
|
|
490
|
+
})
|
|
491
|
+
.catch(function () {
|
|
492
|
+
// Re-render against last known good data; leave DOM as-is on cold
|
|
493
|
+
// start. (Review finding #4.)
|
|
494
|
+
if (window._lastAgents) {
|
|
495
|
+
_safeRender('agents', function() { renderAgents(window._lastAgents); });
|
|
496
|
+
_safeRender('cmdUpdateAgentList', function() { cmdUpdateAgentList(window._lastAgents); });
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
// PRD slice now comes from /api/prd — a dedicated fresh-JSON endpoint
|
|
501
|
+
// that re-runs getPrdInfo() on every request with input-mtime ETag.
|
|
502
|
+
// Returns { status, progress }. prdProgress + the standalone PRD
|
|
503
|
+
// section share one fetch so they always agree on the same tick.
|
|
504
|
+
// /api/prd returns { status, progress }. prdProgress, the standalone PRD
|
|
505
|
+
// section, and the cross-tick item cache all run inside ONE .then so they
|
|
506
|
+
// see the same fresh data on the same tick — previously the standalone
|
|
507
|
+
// renderPrd block ran synchronously, BEFORE this .then resolved, and
|
|
508
|
+
// always saw the prior tick's stash (or undefined on first render).
|
|
509
|
+
// (Review finding #10.)
|
|
510
|
+
_safeRender('prdProgress', function() {
|
|
511
|
+
// Fetch /api/prd and /api/verify-guides IN PARALLEL so renderPrd's
|
|
512
|
+
// verifyGuides lookup uses the SAME tick's guide list as the PRD
|
|
513
|
+
// progress data. Previously /api/verify-guides was fetched separately
|
|
514
|
+
// by _refreshSidebarCounterSummaries with no ordering guarantee,
|
|
515
|
+
// causing a one-tick lag whenever a fresh guide dropped alongside a
|
|
516
|
+
// PRD status update. (Round-2 review finding #5.)
|
|
517
|
+
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
518
|
+
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
519
|
+
window._lastRequestedSeq.prd = seq;
|
|
520
|
+
Promise.all([
|
|
521
|
+
fetch('/api/prd').then(function (r) { return r.ok ? r.json() : null; }),
|
|
522
|
+
fetch('/api/verify-guides').then(function (r) { return r.ok ? r.json() : null; }),
|
|
523
|
+
])
|
|
524
|
+
.then(function (results) {
|
|
525
|
+
// Drop late arrivals — out-of-order resolution from a prior tick
|
|
526
|
+
// must not clobber the current tick's window._last* state.
|
|
527
|
+
// (Round-2 review finding #6.)
|
|
528
|
+
if (seq < (window._lastRequestedSeq.prd || 0)) return;
|
|
529
|
+
const prdInfo = results[0];
|
|
530
|
+
const guides = results[1];
|
|
531
|
+
const status = prdInfo && prdInfo.status != null ? prdInfo.status : window._lastPrdStatus;
|
|
532
|
+
const progress = prdInfo && prdInfo.progress != null ? prdInfo.progress : window._lastPrdProgress;
|
|
533
|
+
window._lastPrdStatus = status;
|
|
534
|
+
window._lastPrdProgress = progress;
|
|
535
|
+
if (Array.isArray(guides)) window._lastVerifyGuides = guides;
|
|
536
|
+
_safeRender('prdProgress', function() { renderPrdProgress(progress); });
|
|
537
|
+
_safeRender('cachePrdItems', function() { _cachePrdItems(progress); });
|
|
538
|
+
// renderPrd handles null status/progress gracefully (early-returns
|
|
539
|
+
// on falsy prd, uses progress?.items || []). Calling it
|
|
540
|
+
// unconditionally is required for the "last PRD archived" case to
|
|
541
|
+
// clear the previously-rendered list — round-2 #10's guard was
|
|
542
|
+
// too aggressive and trapped stale content on screen.
|
|
543
|
+
// (Round-3 #3.)
|
|
544
|
+
_safeRender('prd', function() { renderPrd(status, progress); });
|
|
545
|
+
})
|
|
546
|
+
.catch(function () {
|
|
547
|
+
// `!== undefined` (NOT a truthy check) so the cache-was-null
|
|
548
|
+
// post-archive state still triggers a re-render against null —
|
|
549
|
+
// renderPrd's own null-handling paints the empty 'No PRD found'
|
|
550
|
+
// affordance. Truthy gating would conflate cache-was-null with
|
|
551
|
+
// never-fetched. (Round-3 #8.)
|
|
552
|
+
if (window._lastPrdProgress !== undefined) {
|
|
553
|
+
_safeRender('prdProgress', function() { renderPrdProgress(window._lastPrdProgress); });
|
|
554
|
+
_safeRender('cachePrdItems', function() { _cachePrdItems(window._lastPrdProgress); });
|
|
555
|
+
_safeRender('prd', function() { renderPrd(window._lastPrdStatus, window._lastPrdProgress); });
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
// Inbox is now sourced from /state/notes/inbox (dir listing + per-file
|
|
560
|
+
// content fetch) instead of the /api/status inbox slice (issue #2949).
|
|
561
|
+
// Falls back to data.inbox when the fetch returns null (offline, network
|
|
562
|
+
// race, dir missing) so the renderer is never starved.
|
|
563
|
+
_safeRender('inbox', function() {
|
|
564
|
+
Promise.resolve(fetchInboxFromDisk())
|
|
565
|
+
.then(function (fresh) {
|
|
566
|
+
const list = fresh && fresh.length ? fresh : (window._lastInbox || []);
|
|
567
|
+
window._lastInbox = list;
|
|
568
|
+
_safeRender('inbox', function() { renderInbox(list); });
|
|
569
|
+
})
|
|
570
|
+
.catch(function () {
|
|
571
|
+
if (window._lastInbox) _safeRender('inbox', function() { renderInbox(window._lastInbox); });
|
|
572
|
+
});
|
|
573
|
+
});
|
|
339
574
|
_changed('projects', data.projects);
|
|
340
575
|
_safeRender('cmdUpdateProjectList', function() { cmdUpdateProjectList(data.projects || []); });
|
|
341
576
|
_safeRender('projects', function() { renderProjects(data.projects || []); });
|
|
@@ -346,18 +581,76 @@ function _processStatusUpdate(data) {
|
|
|
346
581
|
if (typeof renderFre === 'function') {
|
|
347
582
|
_safeRender('fre', function() { renderFre(data); });
|
|
348
583
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
_safeRender('
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
584
|
+
// Notes file (notes.md) is now sourced from /state/notes.md. Same shape
|
|
585
|
+
// as the legacy /api/status notes slice ({ content, updatedAt }); the
|
|
586
|
+
// ETag mtime is parsed out of the response header for updatedAt.
|
|
587
|
+
_safeRender('notes', function() {
|
|
588
|
+
Promise.resolve(fetchNotesFromDisk())
|
|
589
|
+
.then(function (fresh) {
|
|
590
|
+
const notes = fresh || window._lastNotes;
|
|
591
|
+
if (notes) window._lastNotes = notes;
|
|
592
|
+
_safeRender('notes', function() { renderNotes(notes); });
|
|
593
|
+
})
|
|
594
|
+
.catch(function () {
|
|
595
|
+
if (window._lastNotes) _safeRender('notes', function() { renderNotes(window._lastNotes); });
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
// _changed('prd', ...) used to gate a now-deleted synchronous renderPrd
|
|
599
|
+
// block. Dropped along with the renderer (issue #2949 + Round-2 finding
|
|
600
|
+
// #1) — leaving a permanently-stable signature in _sectionCache would
|
|
601
|
+
// silently break any future renderer wired to the same key.
|
|
602
|
+
//
|
|
603
|
+
// F1/F3 cross-slice triggers used to be driven by _changed('workItems',
|
|
604
|
+
// data.workItems) + _changed('prs', data.pullRequests). After the slim,
|
|
605
|
+
// data.workItems / data.pullRequests are both undefined every tick so
|
|
606
|
+
// those signatures never advance and the triggers never fire. They are
|
|
607
|
+
// now fired inline from the /api/work-items + /api/pull-requests .then
|
|
608
|
+
// handlers (see _fireCrossSliceRender below) where the FRESH fetched
|
|
609
|
+
// lists are available. (Round-2 review findings #1 + #2.)
|
|
610
|
+
// Pull requests now come from /api/pull-requests — a dedicated fresh-
|
|
611
|
+
// JSON endpoint that re-runs getPullRequests() server-side on every
|
|
612
|
+
// request with input-mtime ETag (issue #2949).
|
|
613
|
+
_safeRender('prs', function() {
|
|
614
|
+
// Stamp the request with a monotonic seq so late arrivals from a
|
|
615
|
+
// prior tick can't clobber fresh state. (Round-2 review finding #6.)
|
|
616
|
+
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
617
|
+
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
618
|
+
window._lastRequestedSeq.prs = seq;
|
|
619
|
+
fetch('/api/pull-requests')
|
|
620
|
+
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
621
|
+
.then(function (fresh) {
|
|
622
|
+
if (seq < (window._lastRequestedSeq.prs || 0)) return;
|
|
623
|
+
const list = Array.isArray(fresh) ? fresh : (window._lastPullRequests || []);
|
|
624
|
+
const wasFirstNonEmpty = !Array.isArray(window._lastPullRequests);
|
|
625
|
+
window._lastPullRequests = list;
|
|
626
|
+
_safeRender('prs', function() { renderPrs(list); });
|
|
627
|
+
_fireCrossSliceRender('pullRequests', list);
|
|
628
|
+
// First-arrival case: if /api/work-items already fired its
|
|
629
|
+
// cross-slice trigger before _lastPullRequests was populated, the
|
|
630
|
+
// F1 +N follow-up chip on PRs reads a stale cached count. Force
|
|
631
|
+
// a one-shot re-fire by clearing the workItems sig. (Round-3 #7.)
|
|
632
|
+
if (wasFirstNonEmpty && Array.isArray(window._lastWorkItems)) {
|
|
633
|
+
_crossSliceSig.workItems = '';
|
|
634
|
+
_fireCrossSliceRender('workItems', window._lastWorkItems);
|
|
635
|
+
}
|
|
636
|
+
})
|
|
637
|
+
.catch(function () {
|
|
638
|
+
if (Array.isArray(window._lastPullRequests)) _safeRender('prs', function() { renderPrs(window._lastPullRequests); });
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
// Archived PRDs come from /api/archived-prds (issue #2949).
|
|
642
|
+
_safeRender('archiveButtons', function() {
|
|
643
|
+
fetch('/api/archived-prds')
|
|
644
|
+
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
645
|
+
.then(function (fresh) {
|
|
646
|
+
const list = Array.isArray(fresh) ? fresh : (window._lastArchivedPrds || []);
|
|
647
|
+
window._lastArchivedPrds = list;
|
|
648
|
+
_safeRender('archiveButtons', function() { renderArchiveButtons(list); });
|
|
649
|
+
})
|
|
650
|
+
.catch(function () {
|
|
651
|
+
if (Array.isArray(window._lastArchivedPrds)) _safeRender('archiveButtons', function() { renderArchiveButtons(window._lastArchivedPrds); });
|
|
652
|
+
});
|
|
653
|
+
});
|
|
361
654
|
_changed('engine', data.engine);
|
|
362
655
|
if (data.engine) _safeRender('engineStatus', function() { renderEngineStatus(data.engine); });
|
|
363
656
|
_safeRender('engineQuickStats', function() {
|
|
@@ -390,13 +683,71 @@ function _processStatusUpdate(data) {
|
|
|
390
683
|
_safeRender('adoThrottle', function() { renderAdoThrottleAlert(data.adoThrottle); });
|
|
391
684
|
_changed('ghThrottle', data.ghThrottle);
|
|
392
685
|
_safeRender('ghThrottle', function() { renderGhThrottleAlert(data.ghThrottle); });
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
_safeRender('
|
|
398
|
-
|
|
399
|
-
|
|
686
|
+
// Dispatch now comes from /api/dispatch — a dedicated fresh-JSON
|
|
687
|
+
// endpoint that re-runs getDispatchQueue() server-side on every
|
|
688
|
+
// request (issue #2949). Completion-report sidecars are now loaded
|
|
689
|
+
// server-side too, so the client no longer needs the cached overlay.
|
|
690
|
+
_safeRender('dispatch', function() {
|
|
691
|
+
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
692
|
+
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
693
|
+
window._lastRequestedSeq.dispatch = seq;
|
|
694
|
+
fetch('/api/dispatch')
|
|
695
|
+
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
696
|
+
.then(function (fresh) {
|
|
697
|
+
if (seq < (window._lastRequestedSeq.dispatch || 0)) return;
|
|
698
|
+
const d = fresh || window._lastDispatch;
|
|
699
|
+
if (d) window._lastDispatch = d;
|
|
700
|
+
_safeRender('dispatch', function() { renderDispatch(d); });
|
|
701
|
+
// Dispatch is the third cross-slice edge: derivePlanStatus reads
|
|
702
|
+
// window._lastDispatch.active/.pending for executing/converting
|
|
703
|
+
// plan-status badges. Without this trigger, plan-status pills lag
|
|
704
|
+
// by ticks when a dispatch is added/completed but no WI or PR
|
|
705
|
+
// delta accompanies it. (Round-3 #5.)
|
|
706
|
+
_fireCrossSliceRender('dispatch', d);
|
|
707
|
+
})
|
|
708
|
+
.catch(function () {
|
|
709
|
+
if (window._lastDispatch) _safeRender('dispatch', function() { renderDispatch(window._lastDispatch); });
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
// prunePrdRequeueState moved into the /api/work-items .then handler so it
|
|
713
|
+
// operates on the fresh fetched list rather than the synchronously-empty
|
|
714
|
+
// window._lastWorkItems. (Review finding #5.)
|
|
715
|
+
// Engine log (last 50 entries) comes from /state/engine/log.json with a
|
|
716
|
+
// client-side .slice(-50) to mirror queries.js getEngineLog (issue #2949).
|
|
717
|
+
_safeRender('engineLog', function() {
|
|
718
|
+
Promise.resolve(fetchEngineLogFromDisk())
|
|
719
|
+
.then(function (fresh) {
|
|
720
|
+
const list = fresh && fresh.length ? fresh : (window._lastEngineLog || []);
|
|
721
|
+
window._lastEngineLog = list;
|
|
722
|
+
_safeRender('engineLog', function() { renderEngineLog(list); });
|
|
723
|
+
})
|
|
724
|
+
.catch(function () {
|
|
725
|
+
if (Array.isArray(window._lastEngineLog)) _safeRender('engineLog', function() { renderEngineLog(window._lastEngineLog); });
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
// Metrics now come from /api/metrics (issue #2949) — the enrichment
|
|
729
|
+
// join (PR counts, runtime totals) runs server-side fresh on every
|
|
730
|
+
// request with input-mtime ETag.
|
|
731
|
+
_safeRender('metrics', function() {
|
|
732
|
+
// Seq guard: metrics.json is mutated on every dispatch completion +
|
|
733
|
+
// PR/build state change via trackEngineUsage. Out-of-order tick
|
|
734
|
+
// resolution could revert tasksCompleted / USD spend counters to the
|
|
735
|
+
// pre-completion state. (Round-3 #10.)
|
|
736
|
+
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
737
|
+
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
738
|
+
window._lastRequestedSeq.metrics = seq;
|
|
739
|
+
fetch('/api/metrics')
|
|
740
|
+
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
741
|
+
.then(function (fresh) {
|
|
742
|
+
if (seq < (window._lastRequestedSeq.metrics || 0)) return;
|
|
743
|
+
const m = fresh && typeof fresh === 'object' ? fresh : (window._lastMetrics || {});
|
|
744
|
+
window._lastMetrics = m;
|
|
745
|
+
_safeRender('metrics', function() { renderMetrics(m); });
|
|
746
|
+
})
|
|
747
|
+
.catch(function () {
|
|
748
|
+
if (window._lastMetrics) _safeRender('metrics', function() { renderMetrics(window._lastMetrics); });
|
|
749
|
+
});
|
|
750
|
+
});
|
|
400
751
|
// managed-processes panel — ETag-gated so unchanged ticks return 304 with
|
|
401
752
|
// no body (P-6e2a8b13). Sequenced BEFORE the keep-processes call below via
|
|
402
753
|
// .then() so the keep renderer reads a populated managed-PID cache for
|
|
@@ -415,28 +766,150 @@ function _processStatusUpdate(data) {
|
|
|
415
766
|
.catch(function () { /* keep render even if managed fetch failed — getLastItems() returns the last good cache (or []) */ })
|
|
416
767
|
.then(function () { try { renderKeepProcesses(); } catch {} });
|
|
417
768
|
}
|
|
418
|
-
|
|
769
|
+
// Work items now come from /api/work-items — a dedicated fresh-JSON
|
|
770
|
+
// endpoint that re-runs getWorkItems() server-side on every request
|
|
771
|
+
// with input-mtime ETag (issue #2949). The previous /state/ + stale-
|
|
772
|
+
// enrichment-overlay path is gone: enrichment runs server-side fresh,
|
|
773
|
+
// so _pr/_prUrl/_pendingReason/_skipReason are guaranteed current.
|
|
774
|
+
_safeRender('workItems', function() {
|
|
775
|
+
// Stamp the request with a monotonic seq — late arrivals from a prior
|
|
776
|
+
// tick can't clobber fresh state. (Round-2 review finding #6.)
|
|
777
|
+
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
778
|
+
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
779
|
+
window._lastRequestedSeq.workItems = seq;
|
|
780
|
+
fetch('/api/work-items')
|
|
781
|
+
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
782
|
+
.then(function (fresh) {
|
|
783
|
+
if (seq < (window._lastRequestedSeq.workItems || 0)) return;
|
|
784
|
+
const list = Array.isArray(fresh) ? fresh : (window._lastWorkItems || []);
|
|
785
|
+
window._lastWorkItems = list;
|
|
786
|
+
_safeRender('workItems', function() { renderWorkItems(list); });
|
|
787
|
+
// prunePrdRequeueState reads the FRESH fetched list rather than the
|
|
788
|
+
// synchronously-empty window._lastWorkItems. (Review finding #5.)
|
|
789
|
+
_safeRender('prunePrdRequeueState', function() { prunePrdRequeueState(list); });
|
|
790
|
+
// F1/F3 cross-slice trigger fires here so the +N follow-up chip on
|
|
791
|
+
// PRs and the plan-status badges update when WI state actually
|
|
792
|
+
// changes. (Round-2 review findings #1 + #2.)
|
|
793
|
+
_fireCrossSliceRender('workItems', list);
|
|
794
|
+
})
|
|
795
|
+
.catch(function () {
|
|
796
|
+
// Re-render against the last known good list rather than blanking
|
|
797
|
+
// the panel. Skip prunePrdRequeueState entirely on the catch arm —
|
|
798
|
+
// calling it against a stale list could delete optimistic UI chips
|
|
799
|
+
// the user just set between the fetch dispatch and the failure.
|
|
800
|
+
// (Round-2 review finding #8.)
|
|
801
|
+
if (Array.isArray(window._lastWorkItems)) {
|
|
802
|
+
_safeRender('workItems', function() { renderWorkItems(window._lastWorkItems); });
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
});
|
|
419
806
|
_changed('skills', data.skills);
|
|
420
807
|
_safeRender('skills', function() { renderSkills(data.skills || []); });
|
|
421
808
|
_changed('mcpServers', data.mcpServers);
|
|
422
809
|
_safeRender('mcpServers', function() { renderMcpServers(data.mcpServers || []); });
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
810
|
+
// Schedule definitions stay on /api/status (config-derived), but the
|
|
811
|
+
// _lastRun/_lastResult overlay is re-applied client-side from
|
|
812
|
+
// /state/engine/schedule-runs.json so a freshly-fired schedule lights up
|
|
813
|
+
// within one 4 s poll (issue #2949).
|
|
814
|
+
// Schedules now come from /api/schedules — a dedicated fresh-JSON endpoint
|
|
815
|
+
// that merges config.schedules with schedule-runs.json server-side, ETag
|
|
816
|
+
// off input mtimes (issue #2949).
|
|
817
|
+
_safeRender('schedules', function() {
|
|
818
|
+
fetch('/api/schedules')
|
|
819
|
+
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
820
|
+
.then(function (fresh) {
|
|
821
|
+
const list = Array.isArray(fresh) ? fresh : (data.schedules || []);
|
|
822
|
+
window._lastSchedules = list;
|
|
823
|
+
_safeRender('schedules', function() { renderSchedules(list); });
|
|
824
|
+
})
|
|
825
|
+
.catch(function () {
|
|
826
|
+
if (Array.isArray(window._lastSchedules)) _safeRender('schedules', function() { renderSchedules(window._lastSchedules); });
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
// Watches array comes from /state/engine/watches.json (issue #2949).
|
|
830
|
+
_safeRender('watches', function() {
|
|
831
|
+
// Seq guard — watches.json is mutated every 3 ticks by engine/watches.js
|
|
832
|
+
// (mutateWatches on watch-fire / pause). (Round-4 #10.)
|
|
833
|
+
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
834
|
+
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
835
|
+
window._lastRequestedSeq.watches = seq;
|
|
836
|
+
Promise.resolve(fetchWatchesFromDisk())
|
|
837
|
+
.then(function (fresh) {
|
|
838
|
+
if (seq < (window._lastRequestedSeq.watches || 0)) return;
|
|
839
|
+
const list = fresh && fresh.length ? fresh : (window._lastWatches || []);
|
|
840
|
+
window._lastWatches = list;
|
|
841
|
+
_safeRender('watches', function() { renderWatches(list); });
|
|
842
|
+
})
|
|
843
|
+
.catch(function () {
|
|
844
|
+
if (Array.isArray(window._lastWatches)) _safeRender('watches', function() { renderWatches(window._lastWatches); });
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
// Meetings come from /state/meetings (dir listing + per-file fetch),
|
|
848
|
+
// slimmed client-side to match dashboard.js _slimMeetingForStatus.
|
|
849
|
+
// Issue #2949 — same outer-cache staleness fix as work-items + PRs.
|
|
850
|
+
_safeRender('meetings', function() {
|
|
851
|
+
Promise.resolve(fetchMeetingsFromDisk())
|
|
852
|
+
.then(function (fresh) {
|
|
853
|
+
const list = fresh && fresh.length ? fresh : (window._lastMeetings || []);
|
|
854
|
+
window._lastMeetings = list;
|
|
855
|
+
_safeRender('meetings', function() { renderMeetings(list); });
|
|
856
|
+
})
|
|
857
|
+
.catch(function () {
|
|
858
|
+
if (Array.isArray(window._lastMeetings)) _safeRender('meetings', function() { renderMeetings(window._lastMeetings); });
|
|
859
|
+
});
|
|
860
|
+
});
|
|
430
861
|
if (typeof renderPipelines === 'function') {
|
|
431
|
-
|
|
862
|
+
// Definitions stay on /api/status, runs overlay (last 5 per pipeline)
|
|
863
|
+
// is re-applied client-side from /state/engine/pipeline-runs.json so a
|
|
864
|
+
// newly-started run lights the panel within one 4 s poll (issue #2949).
|
|
865
|
+
// Pipelines now come from /api/pipelines — a dedicated fresh-JSON
|
|
866
|
+
// endpoint that merges pipelines/*.json with pipeline-runs.json
|
|
867
|
+
// (last 5 runs per pipeline) server-side (issue #2949).
|
|
868
|
+
_safeRender('pipelines', function() {
|
|
869
|
+
// Seq guard — pipeline-runs.json is mutated on every pipeline run
|
|
870
|
+
// start/completion; out-of-order resolution could revert run state.
|
|
871
|
+
// (Round-4 #10.)
|
|
872
|
+
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
873
|
+
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
874
|
+
window._lastRequestedSeq.pipelines = seq;
|
|
875
|
+
fetch('/api/pipelines')
|
|
876
|
+
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
877
|
+
.then(function (fresh) {
|
|
878
|
+
if (seq < (window._lastRequestedSeq.pipelines || 0)) return;
|
|
879
|
+
const list = Array.isArray(fresh) ? fresh : (window._lastPipelines || []);
|
|
880
|
+
window._lastPipelines = list;
|
|
881
|
+
_safeRender('pipelines', function() { renderPipelines(list); });
|
|
882
|
+
})
|
|
883
|
+
.catch(function () {
|
|
884
|
+
if (Array.isArray(window._lastPipelines)) _safeRender('pipelines', function() { renderPipelines(window._lastPipelines); });
|
|
885
|
+
});
|
|
886
|
+
});
|
|
432
887
|
}
|
|
433
|
-
|
|
434
|
-
_safeRender('pinned', function() {
|
|
435
|
-
|
|
888
|
+
// Pinned notes come from /api/pinned (issue #2949).
|
|
889
|
+
_safeRender('pinned', function() {
|
|
890
|
+
// Seq guard — pinned.md is user-mutated via POST /api/pinned. (Round-4 #10.)
|
|
891
|
+
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
892
|
+
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
893
|
+
window._lastRequestedSeq.pinned = seq;
|
|
894
|
+
fetch('/api/pinned')
|
|
895
|
+
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
896
|
+
.then(function (fresh) {
|
|
897
|
+
if (seq < (window._lastRequestedSeq.pinned || 0)) return;
|
|
898
|
+
const list = Array.isArray(fresh) ? fresh : (window._lastPinned || []);
|
|
899
|
+
window._lastPinned = list;
|
|
900
|
+
_safeRender('pinned', function() { renderPinned(list); });
|
|
901
|
+
})
|
|
902
|
+
.catch(function () {
|
|
903
|
+
if (Array.isArray(window._lastPinned)) _safeRender('pinned', function() { renderPinned(window._lastPinned); });
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
// Sidebar counts — read from the dedicated-endpoint caches; data.workItems
|
|
907
|
+
// / data.pullRequests are undefined post-slim. Renders empty until the
|
|
908
|
+
// first /api/work-items + /api/pull-requests fetches resolve.
|
|
436
909
|
const swi = document.getElementById('sidebar-wi');
|
|
437
|
-
if (swi) swi.textContent = (
|
|
910
|
+
if (swi) swi.textContent = (window._lastWorkItems || []).length || '';
|
|
438
911
|
const spr = document.getElementById('sidebar-pr');
|
|
439
|
-
if (spr) spr.textContent = (
|
|
912
|
+
if (spr) spr.textContent = (window._lastPullRequests || []).length || '';
|
|
440
913
|
// Refresh KB and plans every status cycle (~4s) so plan status flips
|
|
441
914
|
// (approve/archive/complete) and KB additions surface within the SPA's
|
|
442
915
|
// 4s poll target. Server-side caches keep this cheap:
|
|
@@ -450,26 +923,11 @@ function _processStatusUpdate(data) {
|
|
|
450
923
|
_safeRender('refreshKnowledgeBase', function() { refreshKnowledgeBase(); });
|
|
451
924
|
_safeRender('refreshPlans', function() { refreshPlans(); });
|
|
452
925
|
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
// or for a PR object to mutate (F1). Runs after the window._last*
|
|
459
|
-
// assignments above so the cached globals these renderers consult are fresh.
|
|
460
|
-
if (_workItemsChanged && !_prsChanged) {
|
|
461
|
-
// F1: only the work-item slice moved this tick — renderPrs wasn't called
|
|
462
|
-
// above, so the +N follow-up chip would otherwise stay stale until the
|
|
463
|
-
// next PR mutation.
|
|
464
|
-
_safeRender('prs:cross-slice', function() { renderPrs(data.pullRequests || []); });
|
|
465
|
-
}
|
|
466
|
-
if ((_workItemsChanged || _prsChanged) && Array.isArray(window._lastPlans) && typeof renderPlans === 'function') {
|
|
467
|
-
// F3: derivePlanStatus + _renderVerifyBadge derive from pullRequests +
|
|
468
|
-
// workItems. Re-render against cached plans so plan status flips within
|
|
469
|
-
// one /api/status tick (~4s) instead of one refreshPlans poll. No-op
|
|
470
|
-
// until _lastPlans is populated.
|
|
471
|
-
_safeRender('plans:cross-slice', function() { renderPlans(window._lastPlans); });
|
|
472
|
-
}
|
|
926
|
+
// F1/F3 cross-slice triggers moved into the /api/work-items + /api/pull-
|
|
927
|
+
// requests .then handlers (see _fireCrossSliceRender helper). The
|
|
928
|
+
// synchronous-tick versions used to fire off _changed signatures derived
|
|
929
|
+
// from data.workItems / data.pullRequests, both undefined after the
|
|
930
|
+
// /api/status slim. (Round-2 review findings #1 + #2.)
|
|
473
931
|
|
|
474
932
|
// Sidebar activity indicators — show red dot on pages with new activity
|
|
475
933
|
try {
|
|
@@ -482,22 +940,12 @@ function _processStatusUpdate(data) {
|
|
|
482
940
|
} catch { /* expected on first load */ }
|
|
483
941
|
}
|
|
484
942
|
|
|
943
|
+
// Auto-reload policy (2026-05-29):
|
|
944
|
+
// Reload only on dashboardStartedAt change (signal that `minions restart`
|
|
945
|
+
// happened and the process is new). dashboardBuildId / runningCommit /
|
|
946
|
+
// installId mismatch DO NOT auto-reload anymore — code edits and engine
|
|
947
|
+
// commits must not silently interrupt the operator's open session.
|
|
485
948
|
let _knownDashboardStartId = null;
|
|
486
|
-
// Hard-reload trigger when the assembled dashboard HTML (and therefore any
|
|
487
|
-
// renderer body) has changed since the page was first loaded. Mirrors the
|
|
488
|
-
// _knownDashboardStartId pattern (R3, W-mpgb0xgc000hf1d3): catches restart-
|
|
489
|
-
// crossing renderer drift that RENDER_VERSIONS (in-process bump) can't see,
|
|
490
|
-
// since a server restart wipes the JS module identity entirely. data.version.
|
|
491
|
-
// dashboardBuildId is the md5 of the assembled HTML — bumps automatically on
|
|
492
|
-
// hot-reload + on cold restart with any /dashboard/** change.
|
|
493
|
-
let _knownDashboardBuildId = null;
|
|
494
|
-
// Hard-reload trigger when the *engine* commit changes — `minions restart`
|
|
495
|
-
// after a `git pull` swaps engine code but doesn't touch any
|
|
496
|
-
// /dashboard/** asset, so `dashboardBuildId` stays the same and the
|
|
497
|
-
// existing buildId reload above never fires. Without this, the stale
|
|
498
|
-
// "Engine running v… — disk has v…" banner persists in already-open tabs
|
|
499
|
-
// until the (hourly by default) /api/version repoll catches up.
|
|
500
|
-
let _knownEngineRunningCommit = null;
|
|
501
949
|
// /api/status ETag cache (W-mpehsyhv0017085a). The dashboard polls every 4 s
|
|
502
950
|
// but the server-side cache only changes every 10–60 s. Sending If-None-Match
|
|
503
951
|
// lets the server short-circuit ~60 %+ of polls into a 304 with no body —
|
|
@@ -761,7 +1209,19 @@ async function refresh() {
|
|
|
761
1209
|
_diagEntry.bytes_received = cl != null && cl !== '' ? Number(cl) : null;
|
|
762
1210
|
}
|
|
763
1211
|
}
|
|
764
|
-
// Auto-reload
|
|
1212
|
+
// Auto-reload policy (2026-05-29):
|
|
1213
|
+
// - dashboardStartedAt change → reload (covers `minions restart`)
|
|
1214
|
+
// - dashboardBuildId change → DO NOT reload (dashboard/** edits
|
|
1215
|
+
// while dev'ing the UI must not silently kick connected tabs)
|
|
1216
|
+
// - runningCommit change → DO NOT reload (engine git-pull is
|
|
1217
|
+
// not a dashboard restart — engine code change doesn't justify
|
|
1218
|
+
// interrupting an open dashboard session)
|
|
1219
|
+
// - installId change → DO NOT reload (MINIONS_HOME swap
|
|
1220
|
+
// is the operator's call to refresh, not ours)
|
|
1221
|
+
//
|
|
1222
|
+
// The user explicitly wants `minions restart` to reload the tab so
|
|
1223
|
+
// they pick up any updated code; they do NOT want any other code-
|
|
1224
|
+
// change signal to trigger a reload.
|
|
765
1225
|
const dashId = (data.version && data.version.dashboardStartedAt) || null;
|
|
766
1226
|
if (dashId && _knownDashboardStartId && dashId !== _knownDashboardStartId) {
|
|
767
1227
|
console.log('Dashboard restarted — reloading page');
|
|
@@ -769,31 +1229,7 @@ async function refresh() {
|
|
|
769
1229
|
return;
|
|
770
1230
|
}
|
|
771
1231
|
if (dashId) _knownDashboardStartId = dashId;
|
|
772
|
-
//
|
|
773
|
-
// any other concat'd JS) — catches drift the in-process RENDER_VERSIONS
|
|
774
|
-
// bump can't see (R3, W-mpgb0xgc000hf1d3).
|
|
775
|
-
const buildId = (data.version && data.version.dashboardBuildId) || null;
|
|
776
|
-
if (buildId && _knownDashboardBuildId && buildId !== _knownDashboardBuildId) {
|
|
777
|
-
console.log('Dashboard build changed — reloading page');
|
|
778
|
-
location.reload();
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
if (buildId) _knownDashboardBuildId = buildId;
|
|
782
|
-
// Auto-reload when the engine's commit changed (engine code was updated
|
|
783
|
-
// and process restarted, e.g. via `minions restart` after `git pull`).
|
|
784
|
-
// dashboardBuildId only hashes HTML/JS/CSS assets, so engine-only code
|
|
785
|
-
// updates don't trigger that path — leaving stale "Engine running v… —
|
|
786
|
-
// disk has v…" banners until the hourly /api/version repoll.
|
|
787
|
-
const engineCommit = (data.version && data.version.runningCommit) || null;
|
|
788
|
-
if (engineCommit && _knownEngineRunningCommit && engineCommit !== _knownEngineRunningCommit) {
|
|
789
|
-
console.log('Engine code changed — reloading page');
|
|
790
|
-
location.reload();
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
if (engineCommit) _knownEngineRunningCommit = engineCommit;
|
|
794
|
-
// Successful poll — clear unreachable state. Placed AFTER the reload
|
|
795
|
-
// guards above so a dashboard restart still triggers location.reload()
|
|
796
|
-
// instead of just dismissing the banner.
|
|
1232
|
+
// Successful poll — clear unreachable state.
|
|
797
1233
|
_lastStatusOkAt = Date.now();
|
|
798
1234
|
_consecutiveStatusFails = 0;
|
|
799
1235
|
_nextPollAllowedAt = 0;
|
|
@@ -812,7 +1248,9 @@ async function refresh() {
|
|
|
812
1248
|
_diagEntry.render_duration_ms = Date.now() - _renderStart;
|
|
813
1249
|
_diagEntry.changed = _diagChanges;
|
|
814
1250
|
_diagEntry.workItems_changed = !!(_diagChanges && _diagChanges.workItems);
|
|
815
|
-
|
|
1251
|
+
// Post-slim, data.workItems is undefined; read from the dedicated
|
|
1252
|
+
// endpoint cache.
|
|
1253
|
+
_diagEntry.workItems_count = (window._lastWorkItems || []).length;
|
|
816
1254
|
_diagEntry.statusCacheVersion = (data.version && data.version.statusCacheVersion) != null
|
|
817
1255
|
? data.version.statusCacheVersion
|
|
818
1256
|
: null;
|