@yemi33/minions 0.1.2432 → 0.1.2433
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 +30 -0
- package/dashboard/js/settings.js +11 -6
- package/dashboard/slim/js/modals-tiles.js +149 -24
- package/dashboard/slim/styles.css +8 -0
- package/dashboard.js +56 -3
- package/package.json +1 -1
package/dashboard/js/refresh.js
CHANGED
|
@@ -1153,6 +1153,34 @@ let _lastStatusData = null;
|
|
|
1153
1153
|
// fresh state anyway.
|
|
1154
1154
|
let _refreshInFlight = false;
|
|
1155
1155
|
|
|
1156
|
+
// ── Embedded-in-slim polling suspend (W-ms3bbzry000i6612) ────────────────
|
|
1157
|
+
// The slim cockpit's Queued-work / Active-PRs (and engine/plans) tiles embed
|
|
1158
|
+
// this classic SPA in a chrome-off ?embed=1 iframe that is now CACHED and
|
|
1159
|
+
// kept mounted (shown/hidden) instead of cold-booted on every open. A hidden
|
|
1160
|
+
// (display:none) iframe still reports document.visibilityState === 'visible',
|
|
1161
|
+
// so without an explicit signal this embedded SPA would keep polling
|
|
1162
|
+
// /api/status every 4s in the background forever — trading a per-open cold
|
|
1163
|
+
// boot for N permanent background pollers. The slim parent posts
|
|
1164
|
+
// {source:'minions-slim-embed', type:'visibility', visible} whenever it
|
|
1165
|
+
// shows/hides the iframe; we suspend the poll while hidden and resume (with an
|
|
1166
|
+
// immediate refresh for freshness) on show. Installed ONLY in embed mode, so
|
|
1167
|
+
// standalone /work, /prs, /engine, /plans, /settings are 100% unaffected.
|
|
1168
|
+
let _embedPollSuspended = false;
|
|
1169
|
+
(function _installSlimEmbedVisibilityBridge() {
|
|
1170
|
+
try {
|
|
1171
|
+
if (!document.documentElement.classList.contains('embed')) return;
|
|
1172
|
+
window.addEventListener('message', function (ev) {
|
|
1173
|
+
// Same-origin only — reject cross-origin/foreign frames.
|
|
1174
|
+
if (!ev || ev.origin !== window.location.origin) return;
|
|
1175
|
+
const d = ev.data;
|
|
1176
|
+
if (!d || d.source !== 'minions-slim-embed' || d.type !== 'visibility') return;
|
|
1177
|
+
_embedPollSuspended = !d.visible;
|
|
1178
|
+
if (d.visible) { try { refresh(); } catch (e) { /* refresh not yet defined / transient */ } }
|
|
1179
|
+
});
|
|
1180
|
+
} catch (e) { /* defensive — never block boot on this bridge */ }
|
|
1181
|
+
})();
|
|
1182
|
+
|
|
1183
|
+
|
|
1156
1184
|
// ── Dashboard-unreachable detector ───────────────────────────────────────
|
|
1157
1185
|
// When the dashboard process dies, /api/status throws or 5xxs and the
|
|
1158
1186
|
// existing catch block just console.errors — the page keeps painting the
|
|
@@ -1340,6 +1368,8 @@ async function refresh(opts) {
|
|
|
1340
1368
|
// dashboard isn't hammered at the steady 4s cadence (which produces
|
|
1341
1369
|
// console-spam and adds load to whatever's wedged).
|
|
1342
1370
|
if (_nextPollAllowedAt && Date.now() < _nextPollAllowedAt) return;
|
|
1371
|
+
// Embedded + hidden in the slim cockpit: skip polling (see the bridge above).
|
|
1372
|
+
if (_embedPollSuspended) return;
|
|
1343
1373
|
_refreshInFlight = true;
|
|
1344
1374
|
const _t0 = Date.now();
|
|
1345
1375
|
const _gap = _prevRefreshTs ? _t0 - _prevRefreshTs : null;
|
package/dashboard/js/settings.js
CHANGED
|
@@ -272,15 +272,20 @@ async function openSettings() {
|
|
|
272
272
|
const slimUxEnabled = slimUxFeature
|
|
273
273
|
? (slimUxFeature.enabled != null ? !!slimUxFeature.enabled : !!slimUxFeature.default)
|
|
274
274
|
: false;
|
|
275
|
+
// W-ms25rsk2 — mirror the Font Size cell's vertical rhythm so the two
|
|
276
|
+
// Appearance controls read as a coherent pair: lead with an equivalent small
|
|
277
|
+
// muted label heading ('Slim UX'), then the checkbox row, then a matching
|
|
278
|
+
// muted description below. Previously this cell led straight with the flex
|
|
279
|
+
// checkbox row, so its title sat above the Font Size <label> and the toggle
|
|
280
|
+
// looked misaligned within the grid.
|
|
275
281
|
const slimUxControl = slimUxFeature
|
|
276
282
|
? '<div data-search="slim ux appearance experimental cockpit">' +
|
|
277
|
-
'<label style="
|
|
278
|
-
|
|
279
|
-
'<
|
|
280
|
-
|
|
281
|
-
'<span style="font-size:var(--text-xs);color:var(--muted);display:block;margin-top:1px">Experimental focused single-screen cockpit. Persists immediately.</span>' +
|
|
282
|
-
'</span>' +
|
|
283
|
+
'<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Slim UX</label>' +
|
|
284
|
+
'<label style="display:flex;align-items:center;gap:8px;cursor:pointer">' +
|
|
285
|
+
'<input type="checkbox" data-feature-toggle="slim-ux"' + (slimUxEnabled ? ' checked' : '') + ' style="accent-color:var(--blue);width:16px;height:16px;cursor:pointer;flex-shrink:0">' +
|
|
286
|
+
'<span style="font-size:var(--text-md);color:var(--text)">Enable slim cockpit</span>' +
|
|
283
287
|
'</label>' +
|
|
288
|
+
'<div style="font-size:var(--text-sm);color:var(--muted);margin-top:1px">Experimental focused single-screen cockpit. Persists immediately.</div>' +
|
|
284
289
|
'</div>'
|
|
285
290
|
: '';
|
|
286
291
|
const paneAppearance =
|
|
@@ -13,8 +13,14 @@
|
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
15
|
function clearTileModalBody() {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// Reuse contract (W-ms3bbzry000i6612): the embed tiles' iframes are cached
|
|
17
|
+
// and kept mounted for an instant reopen, so we DON'T wipe the modal body.
|
|
18
|
+
// Instead pause + hide the active embed iframe and clear only the transient
|
|
19
|
+
// card container. Guarded with typeof so the source-extracted unit-test copy
|
|
20
|
+
// of this fn (which lacks the embed helpers/host) stays a safe no-op.
|
|
21
|
+
if (typeof _pauseActiveEmbed === 'function') _pauseActiveEmbed();
|
|
22
|
+
var transient = document.getElementById('slim-tile-transient');
|
|
23
|
+
if (transient) transient.textContent = '';
|
|
18
24
|
}
|
|
19
25
|
// Stop the agent-detail "Working for" ticker on every dismiss path so the
|
|
20
26
|
// 1s interval started in openAgentDetail can't leak after the modal closes.
|
|
@@ -85,6 +91,131 @@
|
|
|
85
91
|
return card;
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
// ── Reusable embed machinery (W-ms3bbzry000i6612) ──────────────────
|
|
95
|
+
// Each single-iframe embed tile (engine · queued · plans · prs) gets ONE
|
|
96
|
+
// cached iframe that is created on first use and thereafter shown/hidden —
|
|
97
|
+
// NEVER reparented or removed (reparenting/removing an <iframe> discards and
|
|
98
|
+
// reloads its browsing context, which would re-cold-boot the whole embedded
|
|
99
|
+
// SPA and defeat the point). Reopening a tile becomes an instant show instead
|
|
100
|
+
// of a full SPA cold-boot + first full fetch/render.
|
|
101
|
+
//
|
|
102
|
+
// A kept-alive hidden iframe would otherwise keep polling /api/status every 4s
|
|
103
|
+
// in the background: a display:none iframe still reports
|
|
104
|
+
// document.visibilityState === 'visible', so the embedded classic refresh loop
|
|
105
|
+
// has no idea it is off-screen. We tell it explicitly with a postMessage the
|
|
106
|
+
// classic SPA listens for (refresh.js, embed-mode only) — suspend while hidden,
|
|
107
|
+
// resume (with an immediate refresh for freshness) on show. So we trade a
|
|
108
|
+
// per-open cold-boot for a single background boot, not N permanent pollers.
|
|
109
|
+
//
|
|
110
|
+
// The cached iframes live in a persistent #slim-tile-embed-host inside the
|
|
111
|
+
// tile modal body that openTileModal/clearTileModalBody never wipe; transient
|
|
112
|
+
// card content (the dispatches tile) lives in a sibling #slim-tile-transient.
|
|
113
|
+
var _embedFrames = {}; // key -> iframe (cached; never removed/reparented)
|
|
114
|
+
var _activeEmbedKey = null; // key of the currently-shown embed iframe (or null)
|
|
115
|
+
// Single-iframe embed tiles (excludes 'automation', which is its own tabbed,
|
|
116
|
+
// already-lazy composite). key -> [src, title, className].
|
|
117
|
+
var _EMBED_TILES = {
|
|
118
|
+
engine: ['/engine?embed=1', 'Engine', 'slim-engine-embed'],
|
|
119
|
+
queued: ['/work?embed=1', 'Work Items', 'slim-work-embed'],
|
|
120
|
+
plans: ['/plans?embed=1', 'Plans', 'slim-plans-embed'],
|
|
121
|
+
prs: ['/prs?embed=1', 'Pull Requests', 'slim-prs-embed'],
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Tell an embedded classic SPA whether its iframe is currently visible so it
|
|
125
|
+
// can suspend/resume its 4s poll. Same-origin, so contentWindow is reachable;
|
|
126
|
+
// guarded defensively. No-op before the iframe has a live contentWindow.
|
|
127
|
+
function _postEmbedVisibility(frame, visible) {
|
|
128
|
+
try {
|
|
129
|
+
if (frame && frame.contentWindow) {
|
|
130
|
+
frame.contentWindow.postMessage(
|
|
131
|
+
{ source: 'minions-slim-embed', type: 'visibility', visible: !!visible }, '*');
|
|
132
|
+
}
|
|
133
|
+
} catch (e) { /* defensive — same-origin means this should never throw */ }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Ensure the persistent host + transient containers exist inside the tile
|
|
137
|
+
// modal body (created once, never wiped). Returns { host, transient }.
|
|
138
|
+
function _ensureTileContainers() {
|
|
139
|
+
var body = document.getElementById('slim-tile-body');
|
|
140
|
+
if (!body) return null;
|
|
141
|
+
var host = document.getElementById('slim-tile-embed-host');
|
|
142
|
+
if (!host) {
|
|
143
|
+
host = document.createElement('div');
|
|
144
|
+
host.id = 'slim-tile-embed-host';
|
|
145
|
+
host.className = 'slim-tile-embed-host';
|
|
146
|
+
body.appendChild(host);
|
|
147
|
+
}
|
|
148
|
+
var transient = document.getElementById('slim-tile-transient');
|
|
149
|
+
if (!transient) {
|
|
150
|
+
transient = document.createElement('div');
|
|
151
|
+
transient.id = 'slim-tile-transient';
|
|
152
|
+
transient.className = 'slim-tile-transient';
|
|
153
|
+
body.appendChild(transient);
|
|
154
|
+
}
|
|
155
|
+
return { host: host, transient: transient };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Create (once) the cached iframe for an embed tile and return it. Built with
|
|
159
|
+
// createElement (no innerHTML) to satisfy the dashboard no-unsanitized lint
|
|
160
|
+
// gate. Starts hidden; a load-time visibility message pauses its poll until
|
|
161
|
+
// the tile is actually shown.
|
|
162
|
+
function _getOrCreateEmbedFrame(key) {
|
|
163
|
+
if (_embedFrames[key]) return _embedFrames[key];
|
|
164
|
+
var spec = _EMBED_TILES[key];
|
|
165
|
+
if (!spec) return null;
|
|
166
|
+
var containers = _ensureTileContainers();
|
|
167
|
+
if (!containers) return null;
|
|
168
|
+
var frame = document.createElement('iframe');
|
|
169
|
+
frame.className = spec[2];
|
|
170
|
+
frame.src = spec[0];
|
|
171
|
+
frame.title = spec[1];
|
|
172
|
+
frame.hidden = true;
|
|
173
|
+
frame.addEventListener('load', function() {
|
|
174
|
+
_postEmbedVisibility(frame, _activeEmbedKey === key);
|
|
175
|
+
});
|
|
176
|
+
containers.host.appendChild(frame);
|
|
177
|
+
_embedFrames[key] = frame;
|
|
178
|
+
return frame;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Show one embed tile's cached iframe and hide the rest, pausing/resuming
|
|
182
|
+
// each one's embedded poll to match. Reused by every single-iframe embed tile.
|
|
183
|
+
function _showReusableEmbed(key) {
|
|
184
|
+
_getOrCreateEmbedFrame(key);
|
|
185
|
+
Object.keys(_embedFrames).forEach(function(k) {
|
|
186
|
+
var f = _embedFrames[k];
|
|
187
|
+
if (!f) return;
|
|
188
|
+
var on = (k === key);
|
|
189
|
+
f.hidden = !on;
|
|
190
|
+
_postEmbedVisibility(f, on);
|
|
191
|
+
});
|
|
192
|
+
_activeEmbedKey = key;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Hide + pause whichever embed iframe is currently shown (kept mounted for an
|
|
196
|
+
// instant reopen). Called on modal close and when switching to a card tile.
|
|
197
|
+
function _pauseActiveEmbed() {
|
|
198
|
+
Object.keys(_embedFrames).forEach(function(k) {
|
|
199
|
+
var f = _embedFrames[k];
|
|
200
|
+
if (!f) return;
|
|
201
|
+
if (!f.hidden) _postEmbedVisibility(f, false);
|
|
202
|
+
f.hidden = true;
|
|
203
|
+
});
|
|
204
|
+
_activeEmbedKey = null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Prewarm the two hot embed tiles (Queued work + Active PRs) shortly after the
|
|
208
|
+
// slim cockpit paints, so their first open is an instant show rather than a
|
|
209
|
+
// cold boot. Deferred so it doesn't compete with initial paint; engine/plans
|
|
210
|
+
// are created lazily on first open (and cached/reused thereafter).
|
|
211
|
+
function prewarmHotEmbeds() {
|
|
212
|
+
_getOrCreateEmbedFrame('queued');
|
|
213
|
+
_getOrCreateEmbedFrame('prs');
|
|
214
|
+
}
|
|
215
|
+
(function schedulePrewarm() {
|
|
216
|
+
try { setTimeout(prewarmHotEmbeds, 800); } catch (e) { /* no timers — skip prewarm */ }
|
|
217
|
+
})();
|
|
218
|
+
|
|
88
219
|
// Per-tile body renderers for the detail modal. Each reads the latest status
|
|
89
220
|
// snapshot and fills the modal body; openTileModal looks them up by key.
|
|
90
221
|
// Engine tile body — reuses the LITERAL classic dashboard /engine screen
|
|
@@ -100,11 +231,7 @@
|
|
|
100
231
|
// /engine even with slim-ux ON (only / is taken over). Built with createElement
|
|
101
232
|
// (no innerHTML) to satisfy the dashboard no-unsanitized lint gate.
|
|
102
233
|
function renderEngineBody(body) {
|
|
103
|
-
|
|
104
|
-
frame.className = 'slim-engine-embed';
|
|
105
|
-
frame.src = '/engine?embed=1';
|
|
106
|
-
frame.title = 'Engine';
|
|
107
|
-
body.appendChild(frame);
|
|
234
|
+
_showReusableEmbed('engine');
|
|
108
235
|
}
|
|
109
236
|
|
|
110
237
|
function renderDispatchTileBody(body, data, isActive) {
|
|
@@ -135,11 +262,7 @@
|
|
|
135
262
|
// with slim-ux ON (only / is taken over). Built with createElement (no
|
|
136
263
|
// innerHTML) to satisfy the dashboard no-unsanitized lint gate.
|
|
137
264
|
function renderQueuedWorkBody(body) {
|
|
138
|
-
|
|
139
|
-
frame.className = 'slim-work-embed';
|
|
140
|
-
frame.src = '/work?embed=1';
|
|
141
|
-
frame.title = 'Work Items';
|
|
142
|
-
body.appendChild(frame);
|
|
265
|
+
_showReusableEmbed('queued');
|
|
143
266
|
}
|
|
144
267
|
|
|
145
268
|
// Plans tile body — reuses the LITERAL classic dashboard /plans screen instead
|
|
@@ -154,11 +277,7 @@
|
|
|
154
277
|
// reachable at /plans even with slim-ux ON (only / is taken over). Built with
|
|
155
278
|
// createElement (no innerHTML) to satisfy the dashboard no-unsanitized lint gate.
|
|
156
279
|
function renderPlansBody(body) {
|
|
157
|
-
|
|
158
|
-
frame.className = 'slim-plans-embed';
|
|
159
|
-
frame.src = '/plans?embed=1';
|
|
160
|
-
frame.title = 'Plans';
|
|
161
|
-
body.appendChild(frame);
|
|
280
|
+
_showReusableEmbed('plans');
|
|
162
281
|
}
|
|
163
282
|
|
|
164
283
|
// Pull-requests tile body — reuses the LITERAL classic dashboard /prs screen
|
|
@@ -173,11 +292,7 @@
|
|
|
173
292
|
// at /prs even with slim-ux ON (only / is taken over). Built with createElement
|
|
174
293
|
// (no innerHTML) to satisfy the dashboard no-unsanitized lint gate.
|
|
175
294
|
function renderPrsBody(body) {
|
|
176
|
-
|
|
177
|
-
frame.className = 'slim-prs-embed';
|
|
178
|
-
frame.src = '/prs?embed=1';
|
|
179
|
-
frame.title = 'Pull Requests';
|
|
180
|
-
body.appendChild(frame);
|
|
295
|
+
_showReusableEmbed('prs');
|
|
181
296
|
}
|
|
182
297
|
|
|
183
298
|
// Automation tile body — a tabbed composite (Watches · Schedules · Pipelines)
|
|
@@ -283,7 +398,12 @@
|
|
|
283
398
|
var titleEl = document.getElementById('slim-tile-title');
|
|
284
399
|
var body = document.getElementById('slim-tile-body');
|
|
285
400
|
if (!modal || !body) return;
|
|
286
|
-
body
|
|
401
|
+
// Reuse contract: never wipe the whole body (that would destroy the cached,
|
|
402
|
+
// kept-alive embed iframes). Ensure the persistent host + transient
|
|
403
|
+
// containers, then clear ONLY the transient card container.
|
|
404
|
+
var containers = _ensureTileContainers();
|
|
405
|
+
var target = (containers && containers.transient) || body;
|
|
406
|
+
if (containers) containers.transient.textContent = '';
|
|
287
407
|
if (titleEl) titleEl.textContent = view.title;
|
|
288
408
|
// The "+ Link PR" header chip only applies to the PR list.
|
|
289
409
|
var headerChip = document.getElementById('slim-tile-modal-linkpr');
|
|
@@ -298,7 +418,12 @@
|
|
|
298
418
|
modal.classList.toggle('tile-modal--plans', key === 'plans');
|
|
299
419
|
modal.classList.toggle('tile-modal--prs', key === 'prs');
|
|
300
420
|
modal.classList.toggle('tile-modal--automation', key === 'automation');
|
|
301
|
-
|
|
421
|
+
// Single-iframe embed tiles show their cached iframe (via _showReusableEmbed
|
|
422
|
+
// inside their render fn) and ignore `target`. Card + automation tiles render
|
|
423
|
+
// into the transient container, so pause/hide any shown embed behind them.
|
|
424
|
+
var isReusableEmbed = Object.prototype.hasOwnProperty.call(_EMBED_TILES, key);
|
|
425
|
+
if (!isReusableEmbed) _pauseActiveEmbed();
|
|
426
|
+
view.render(target, lastStatusData || {});
|
|
302
427
|
modal.classList.add('open');
|
|
303
428
|
}
|
|
304
429
|
|
|
@@ -789,6 +789,14 @@
|
|
|
789
789
|
display: block; width: 100%; height: calc(100vh - 96px); min-height: 420px;
|
|
790
790
|
border: 0; background: var(--bg);
|
|
791
791
|
}
|
|
792
|
+
/* Reusable embed host (W-ms3bbzry000i6612): the engine/queued/plans/prs
|
|
793
|
+
embed iframes are cached and kept mounted for an instant reopen, then
|
|
794
|
+
shown/hidden via the [hidden] attribute. The class rules above set
|
|
795
|
+
display:block, which outranks [hidden]'s UA display:none — so hidden
|
|
796
|
+
cached frames must be forced back to none. The id+type+attr selector
|
|
797
|
+
outranks the class rules (no !important needed). */
|
|
798
|
+
.slim-tile-embed-host, .slim-tile-transient { display: block; }
|
|
799
|
+
#slim-tile-embed-host iframe[hidden] { display: none; }
|
|
792
800
|
/* Automation tile (Watches · Schedules · Pipelines) — a tabbed composite
|
|
793
801
|
inside the shared wide tile-modal. The tab bar sits above the embedded
|
|
794
802
|
classic screen, so the iframe height leaves room for it (vs the padless
|
package/dashboard.js
CHANGED
|
@@ -3370,10 +3370,56 @@ async function _maxInputMtimeMs(inputs) {
|
|
|
3370
3370
|
throw err;
|
|
3371
3371
|
}
|
|
3372
3372
|
}
|
|
3373
|
+
// ── Optional list pagination (W-ms3bbzry000i6612) ────────────────────────
|
|
3374
|
+
// Progressive-load foundation for large list endpoints (/api/work-items,
|
|
3375
|
+
// /api/pull-requests). Reads optional `limit`/`offset` query params; when
|
|
3376
|
+
// NEITHER is present the endpoint stays byte-identical to its legacy full-array
|
|
3377
|
+
// response (so /api/status count derivations and the classic 4s refresh loop
|
|
3378
|
+
// are unaffected). When present, the endpoint slices the SAME deterministically
|
|
3379
|
+
// -ordered, already-enriched+cached full list (guaranteeing `total` matches the
|
|
3380
|
+
// full response and pages never overlap or skip) and returns a paged envelope.
|
|
3381
|
+
//
|
|
3382
|
+
// Design note: we slice the cached enriched list at the endpoint rather than
|
|
3383
|
+
// pushing LIMIT/OFFSET into the per-scope stores because (a) the enriched list
|
|
3384
|
+
// is built once per ~1s and shared with /api/status, so store-level paging
|
|
3385
|
+
// would duplicate enrichment and risk diverging from the status counts, (b) the
|
|
3386
|
+
// cross-scope display order is post-enrichment (central + every project by
|
|
3387
|
+
// rowid), which per-scope SQL LIMIT/OFFSET cannot compose, and (c) slicing the
|
|
3388
|
+
// deterministic cached array is O(page) to serialize — the real transfer win.
|
|
3389
|
+
const _LIST_PAGE_MAX = 1000;
|
|
3390
|
+
function _parsePageParams(req) {
|
|
3391
|
+
const params = new URL((req && req.url) || '/', 'http://localhost').searchParams;
|
|
3392
|
+
const hasLimit = params.has('limit');
|
|
3393
|
+
const hasOffset = params.has('offset');
|
|
3394
|
+
if (!hasLimit && !hasOffset) return null; // legacy full-list path
|
|
3395
|
+
const rawLimit = Number(params.get('limit'));
|
|
3396
|
+
const rawOffset = Number(params.get('offset'));
|
|
3397
|
+
const limit = hasLimit && Number.isFinite(rawLimit)
|
|
3398
|
+
? Math.max(1, Math.min(_LIST_PAGE_MAX, Math.floor(rawLimit)))
|
|
3399
|
+
: 50;
|
|
3400
|
+
const offset = hasOffset && Number.isFinite(rawOffset) ? Math.max(0, Math.floor(rawOffset)) : 0;
|
|
3401
|
+
return { limit, offset };
|
|
3402
|
+
}
|
|
3403
|
+
function _paginateList(list, page) {
|
|
3404
|
+
const arr = Array.isArray(list) ? list : [];
|
|
3405
|
+
const total = arr.length;
|
|
3406
|
+
const offset = Math.max(0, page.offset | 0);
|
|
3407
|
+
const limit = Math.max(1, page.limit | 0);
|
|
3408
|
+
const items = arr.slice(offset, offset + limit);
|
|
3409
|
+
return { items, total, offset, limit, hasMore: offset + items.length < total };
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3373
3412
|
async function serveFreshJson(req, res, opts) {
|
|
3374
3413
|
const inputs = Array.isArray(opts && opts.inputs) ? opts.inputs : [];
|
|
3375
3414
|
const builder = opts && typeof opts.builder === 'function' ? opts.builder : null;
|
|
3376
3415
|
const tag = opts && opts.tag ? String(opts.tag) : 'v';
|
|
3416
|
+
// Optional response variant discriminator (e.g. pagination window). Folded
|
|
3417
|
+
// into the ETag so a paged request never gets served the full-list cache and
|
|
3418
|
+
// vice versa. Empty string keeps the ETag byte-identical to the legacy value,
|
|
3419
|
+
// so no-variant callers are unaffected (W-ms3bbzry000i6612).
|
|
3420
|
+
const variant = opts && opts.variant ? String(opts.variant) : '';
|
|
3421
|
+
// Optional post-builder transform (e.g. slice a page out of the full list).
|
|
3422
|
+
const transform = opts && typeof opts.transform === 'function' ? opts.transform : null;
|
|
3377
3423
|
if (!builder) {
|
|
3378
3424
|
res.statusCode = 500;
|
|
3379
3425
|
res.setHeader('Content-Type', 'application/json');
|
|
@@ -3382,7 +3428,7 @@ async function serveFreshJson(req, res, opts) {
|
|
|
3382
3428
|
}
|
|
3383
3429
|
const mtime = await _maxInputMtimeMs(inputs);
|
|
3384
3430
|
const eventVersion = _getCurrentEventVersion();
|
|
3385
|
-
const etag = '"' + tag + '-' + mtime + '-' + eventVersion + '"';
|
|
3431
|
+
const etag = '"' + tag + (variant ? '-' + variant : '') + '-' + mtime + '-' + eventVersion + '"';
|
|
3386
3432
|
res.setHeader('ETag', etag);
|
|
3387
3433
|
res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
|
|
3388
3434
|
if (req && req.headers && req.headers['if-none-match'] === etag) {
|
|
@@ -3393,6 +3439,7 @@ async function serveFreshJson(req, res, opts) {
|
|
|
3393
3439
|
let payload;
|
|
3394
3440
|
try {
|
|
3395
3441
|
payload = builder();
|
|
3442
|
+
if (transform) payload = transform(payload);
|
|
3396
3443
|
} catch (e) {
|
|
3397
3444
|
res.statusCode = 500;
|
|
3398
3445
|
res.setHeader('Content-Type', 'application/json');
|
|
@@ -14568,22 +14615,28 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14568
14615
|
builder: () => getAgents(),
|
|
14569
14616
|
});
|
|
14570
14617
|
}},
|
|
14571
|
-
{ method: 'GET', path: '/api/work-items', desc: 'Fully-enriched SQL-backed work items joined with dispatch/PR state — fresh on every request; description hard-capped + acceptanceCriteria/references replaced with *Count integers (W-mq5xg5e9000nec0e); detail modal lazy-loads the full record via GET /api/work-items/<id
|
|
14618
|
+
{ method: 'GET', path: '/api/work-items', desc: 'Fully-enriched SQL-backed work items joined with dispatch/PR state — fresh on every request; description hard-capped + acceptanceCriteria/references replaced with *Count integers (W-mq5xg5e9000nec0e); detail modal lazy-loads the full record via GET /api/work-items/<id>. Optional ?limit=&offset= returns a { items, total, hasMore, offset, limit } page (progressive load, W-ms3bbzry000i6612); with no paging params the full array is byte-identical to legacy.', handler: (req, res) => {
|
|
14619
|
+
const page = _parsePageParams(req);
|
|
14572
14620
|
return serveFreshJson(req, res, {
|
|
14573
14621
|
tag: 'work-items',
|
|
14574
14622
|
inputs: [CONFIG_PATH],
|
|
14623
|
+
variant: page ? ('p' + page.offset + '.' + page.limit) : '',
|
|
14575
14624
|
// W-mq5xg5e9000nec0e — Slim every item before stringify so the polled
|
|
14576
14625
|
// refresh-loop payload stays < ~300 KB even when description fields
|
|
14577
14626
|
// include 100+ KB transcripts. slimWorkItemForList shallow-copies so
|
|
14578
14627
|
// queries.getWorkItems()'s in-memory cache is never mutated.
|
|
14579
14628
|
builder: () => getWorkItems().map(slimWorkItemForList),
|
|
14629
|
+
transform: page ? (list) => _paginateList(list, page) : null,
|
|
14580
14630
|
});
|
|
14581
14631
|
}},
|
|
14582
|
-
{ method: 'GET', path: '/api/pull-requests', desc: 'Fully-enriched SQL-backed pull requests with URL backfill and project stamps', handler: (req, res) => {
|
|
14632
|
+
{ method: 'GET', path: '/api/pull-requests', desc: 'Fully-enriched SQL-backed pull requests with URL backfill and project stamps. Optional ?limit=&offset= returns a { items, total, hasMore, offset, limit } page (progressive load, W-ms3bbzry000i6612); with no paging params the full array is byte-identical to legacy.', handler: (req, res) => {
|
|
14633
|
+
const page = _parsePageParams(req);
|
|
14583
14634
|
return serveFreshJson(req, res, {
|
|
14584
14635
|
tag: 'pull-requests',
|
|
14585
14636
|
inputs: [CONFIG_PATH],
|
|
14637
|
+
variant: page ? ('p' + page.offset + '.' + page.limit) : '',
|
|
14586
14638
|
builder: () => queries.getPullRequests(),
|
|
14639
|
+
transform: page ? (list) => _paginateList(list, page) : null,
|
|
14587
14640
|
});
|
|
14588
14641
|
}},
|
|
14589
14642
|
// GET /api/prs/<id> — single fully-enriched PR record by canonical id
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2433",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|