@yemi33/minions 0.1.13 → 0.1.15
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/CHANGELOG.md +26 -0
- package/dashboard/js/command-center.js +3 -0
- package/dashboard/js/modal-qa.js +41 -0
- package/dashboard/js/render-agents.js +27 -0
- package/dashboard/js/render-dispatch.js +23 -0
- package/dashboard/js/render-inbox.js +37 -0
- package/dashboard/js/render-kb.js +18 -0
- package/dashboard/js/render-plans.js +233 -0
- package/dashboard/js/render-prd.js +220 -0
- package/dashboard/js/render-work-items.js +1 -1
- package/dashboard/js/settings.js +20 -0
- package/dashboard/js/utils.js +3 -0
- package/dashboard-build.js +4 -3
- package/dashboard.js +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.15 (2026-03-28)
|
|
4
|
+
|
|
5
|
+
### Dashboard
|
|
6
|
+
- dashboard/js/command-center.js
|
|
7
|
+
- dashboard/js/render-plans.js
|
|
8
|
+
- dashboard/js/render-prd.js
|
|
9
|
+
- dashboard/js/render-work-items.js
|
|
10
|
+
- dashboard/js/utils.js
|
|
11
|
+
|
|
12
|
+
## 0.1.14 (2026-03-26)
|
|
13
|
+
|
|
14
|
+
### Dashboard
|
|
15
|
+
- dashboard-build.js
|
|
16
|
+
- dashboard.js
|
|
17
|
+
- dashboard/js/modal-qa.js
|
|
18
|
+
- dashboard/js/render-agents.js
|
|
19
|
+
- dashboard/js/render-dispatch.js
|
|
20
|
+
- dashboard/js/render-inbox.js
|
|
21
|
+
- dashboard/js/render-kb.js
|
|
22
|
+
- dashboard/js/render-plans.js
|
|
23
|
+
- dashboard/js/render-prd.js
|
|
24
|
+
- dashboard/js/settings.js
|
|
25
|
+
|
|
26
|
+
### Other
|
|
27
|
+
- test/playwright/dashboard.spec.js
|
|
28
|
+
|
|
3
29
|
## 0.1.13 (2026-03-26)
|
|
4
30
|
|
|
5
31
|
### Dashboard
|
|
@@ -222,6 +222,7 @@ async function ccExecuteAction(action) {
|
|
|
222
222
|
const d = await res.json();
|
|
223
223
|
status.innerHTML = '✓ Dispatched: <strong>' + escHtml(d.id || action.title) + '</strong>';
|
|
224
224
|
status.style.color = 'var(--green)';
|
|
225
|
+
wakeEngine();
|
|
225
226
|
break;
|
|
226
227
|
}
|
|
227
228
|
case 'note': {
|
|
@@ -261,6 +262,7 @@ async function ccExecuteAction(action) {
|
|
|
261
262
|
}
|
|
262
263
|
status.innerHTML = '✓ Retried: <strong>' + escHtml((action.ids || []).join(', ')) + '</strong>';
|
|
263
264
|
status.style.color = 'var(--green)';
|
|
265
|
+
wakeEngine();
|
|
264
266
|
break;
|
|
265
267
|
}
|
|
266
268
|
case 'pause-plan': {
|
|
@@ -338,6 +340,7 @@ async function ccExecuteAction(action) {
|
|
|
338
340
|
});
|
|
339
341
|
status.innerHTML = '✓ Plan execution queued: <strong>' + escHtml(action.file) + '</strong>';
|
|
340
342
|
status.style.color = 'var(--green)';
|
|
343
|
+
wakeEngine();
|
|
341
344
|
refreshPlans();
|
|
342
345
|
break;
|
|
343
346
|
}
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -266,3 +266,44 @@ async function _processQaMessage(message, selection) {
|
|
|
266
266
|
document.getElementById('modal-qa-input')?.focus();
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
|
+
|
|
270
|
+
async function qaNewPrd(planFile) {
|
|
271
|
+
qaDisablePrdButtons();
|
|
272
|
+
const allPlans = window._lastStatus?.plans || [];
|
|
273
|
+
const mdPlan = allPlans.find(p => p.file === planFile);
|
|
274
|
+
const project = mdPlan?.project || '';
|
|
275
|
+
|
|
276
|
+
planExecute(planFile, project, null);
|
|
277
|
+
|
|
278
|
+
const btn = document.getElementById('qa-generate-prd-btn');
|
|
279
|
+
if (btn) btn.innerHTML = '<span style="color:var(--green);font-size:12px">New PRD dispatched — existing work continues, agent creating fresh PRD from revised plan.</span>';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function qaReplacePrd(planFile) {
|
|
283
|
+
qaDisablePrdButtons();
|
|
284
|
+
const allPlans = window._lastStatus?.plans || [];
|
|
285
|
+
const mdPlan = allPlans.find(p => p.file === planFile);
|
|
286
|
+
const project = mdPlan?.project || '';
|
|
287
|
+
const existingPrd = allPlans.find(p => p.file.endsWith('.json') && p.project === project);
|
|
288
|
+
|
|
289
|
+
if (existingPrd) {
|
|
290
|
+
// Pause first to stop materialization, then clean pending items
|
|
291
|
+
try {
|
|
292
|
+
await fetch('/api/plans/pause', {
|
|
293
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
294
|
+
body: JSON.stringify({ file: existingPrd.file })
|
|
295
|
+
});
|
|
296
|
+
} catch {}
|
|
297
|
+
try {
|
|
298
|
+
await fetch('/api/plans/regenerate', {
|
|
299
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
300
|
+
body: JSON.stringify({ source: existingPrd.file })
|
|
301
|
+
});
|
|
302
|
+
} catch {}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
planExecute(planFile, project, null);
|
|
306
|
+
|
|
307
|
+
const btn = document.getElementById('qa-generate-prd-btn');
|
|
308
|
+
if (btn) btn.innerHTML = '<span style="color:var(--orange);font-size:12px">Replacing PRD — old items paused, agent regenerating from revised plan.</span>';
|
|
309
|
+
}
|
|
@@ -15,3 +15,30 @@ function renderAgents(agents) {
|
|
|
15
15
|
</div>
|
|
16
16
|
`).join('');
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
async function openAgentDetail(id) {
|
|
20
|
+
currentAgentId = id;
|
|
21
|
+
const agent = agentData.find(a => a.id === id);
|
|
22
|
+
currentTab = (agent?.status === 'working') ? 'live' : 'thought-process';
|
|
23
|
+
if (!agent) return;
|
|
24
|
+
|
|
25
|
+
document.getElementById('detail-agent-name').innerHTML =
|
|
26
|
+
'<span style="font-size:22px">' + agent.emoji + '</span> ' + agent.name + ' — ' + agent.role;
|
|
27
|
+
|
|
28
|
+
const badgeClass = agent.status;
|
|
29
|
+
document.getElementById('detail-status-line').innerHTML =
|
|
30
|
+
'<span class="status-badge ' + badgeClass + '">' + agent.status.toUpperCase() + '</span> ' +
|
|
31
|
+
'<span style="color:var(--muted)">' + escHtml(agent.lastAction) + '</span>' +
|
|
32
|
+
(agent.resultSummary ? '<div style="margin-top:4px;font-size:11px;color:var(--text);line-height:1.4">' + escHtml(agent.resultSummary.slice(0, 300)) + '</div>' : '');
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const detail = await fetch('/api/agent/' + id).then(r => r.json());
|
|
36
|
+
renderDetailTabs(detail);
|
|
37
|
+
renderDetailContent(detail, currentTab);
|
|
38
|
+
} catch(e) {
|
|
39
|
+
document.getElementById('detail-content').textContent = 'Error loading agent detail: ' + e.message;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
document.getElementById('detail-overlay').classList.add('open');
|
|
43
|
+
document.getElementById('detail-panel').classList.add('open');
|
|
44
|
+
}
|
|
@@ -146,3 +146,26 @@ function shortTime(t) {
|
|
|
146
146
|
if (!t) return '';
|
|
147
147
|
try { return new Date(t).toLocaleTimeString(); } catch { return t; }
|
|
148
148
|
}
|
|
149
|
+
|
|
150
|
+
async function showErrorDetails(agentId, reason, task) {
|
|
151
|
+
document.getElementById('modal-title').textContent = 'Error: ' + task;
|
|
152
|
+
document.getElementById('modal-body').textContent = 'Reason: ' + reason + '\n\nLoading agent output...';
|
|
153
|
+
document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
|
|
154
|
+
document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
|
|
155
|
+
document.getElementById('modal').classList.add('open');
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const output = await fetch('/api/agent/' + agentId + '/output').then(r => r.text());
|
|
159
|
+
const lines = output.split('\n');
|
|
160
|
+
const stderrIdx = lines.findIndex(l => l.startsWith('## stderr'));
|
|
161
|
+
let summary = '';
|
|
162
|
+
if (stderrIdx >= 0) {
|
|
163
|
+
const stderr = lines.slice(stderrIdx + 1).join('\n').trim();
|
|
164
|
+
if (stderr) summary = 'STDERR:\n' + stderr.slice(-2000);
|
|
165
|
+
}
|
|
166
|
+
if (!summary) summary = output.slice(-3000);
|
|
167
|
+
document.getElementById('modal-body').textContent = 'Reason: ' + reason + '\n\n---\n\n' + summary;
|
|
168
|
+
} catch {
|
|
169
|
+
document.getElementById('modal-body').textContent = 'Reason: ' + reason + '\n\n(Could not load agent output)';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -124,3 +124,40 @@ function modalCancelEdit() {
|
|
|
124
124
|
document.getElementById('modal-save-btn').style.display = 'none';
|
|
125
125
|
document.getElementById('modal-cancel-edit-btn').style.display = 'none';
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
async function deleteInboxItem(name) {
|
|
129
|
+
if (!confirm('Delete "' + name + '" from inbox?')) return;
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetch('/api/inbox/delete', {
|
|
132
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
133
|
+
body: JSON.stringify({ name })
|
|
134
|
+
});
|
|
135
|
+
if (res.ok) { refresh(); } else { const d = await res.json(); alert('Failed: ' + (d.error || 'unknown')); }
|
|
136
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function openInboxInExplorer(name) {
|
|
140
|
+
try {
|
|
141
|
+
await fetch('/api/inbox/open', {
|
|
142
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
143
|
+
body: JSON.stringify({ name })
|
|
144
|
+
});
|
|
145
|
+
} catch {}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function doPromoteToKB(name, category) {
|
|
149
|
+
try {
|
|
150
|
+
const res = await fetch('/api/inbox/promote-kb', {
|
|
151
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
152
|
+
body: JSON.stringify({ name, category })
|
|
153
|
+
});
|
|
154
|
+
const data = await res.json();
|
|
155
|
+
if (res.ok) {
|
|
156
|
+
closeModal();
|
|
157
|
+
refresh();
|
|
158
|
+
refreshKnowledgeBase();
|
|
159
|
+
} else {
|
|
160
|
+
alert('Failed: ' + (data.error || 'unknown'));
|
|
161
|
+
}
|
|
162
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
163
|
+
}
|
|
@@ -105,3 +105,21 @@ async function kbSweep() {
|
|
|
105
105
|
}
|
|
106
106
|
setTimeout(() => { btn.textContent = origText; btn.style.color = 'var(--muted)'; btn.disabled = false; }, 3000);
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
async function kbOpenItem(category, file) {
|
|
110
|
+
try {
|
|
111
|
+
const content = await fetch('/api/knowledge/' + category + '/' + encodeURIComponent(file)).then(r => r.text());
|
|
112
|
+
const display = content.replace(/^---[\s\S]*?---\n*/m, '');
|
|
113
|
+
document.getElementById('modal-title').textContent = file;
|
|
114
|
+
document.getElementById('modal-body').textContent = display;
|
|
115
|
+
_modalDocContext = { title: file, content: display, selection: '' };
|
|
116
|
+
_modalFilePath = 'knowledge/' + category + '/' + file; showModalQa();
|
|
117
|
+
// Clear notification badge when opening this document
|
|
118
|
+
const card = findCardForFile(_modalFilePath);
|
|
119
|
+
if (card) clearNotifBadge(card);
|
|
120
|
+
// steer btn removed — unified send
|
|
121
|
+
document.getElementById('modal').classList.add('open');
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.error('Failed to load KB item:', e);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -269,6 +269,7 @@ async function planExecute(file, project, btn) {
|
|
|
269
269
|
}
|
|
270
270
|
closeModal();
|
|
271
271
|
showToast('cmd-toast', data.alreadyQueued ? 'Already queued (' + data.id + ')' : 'Queued ' + data.id + ' — agent will convert plan to PRD', true);
|
|
272
|
+
wakeEngine();
|
|
272
273
|
refreshPlans();
|
|
273
274
|
} else {
|
|
274
275
|
if (btn) { btn.textContent = 'Execute'; btn.disabled = false; btn.style.color = 'var(--green)'; }
|
|
@@ -302,3 +303,235 @@ function planHideRevise(file) {
|
|
|
302
303
|
const id = 'revise-input-' + file.replace(/\./g, '-');
|
|
303
304
|
document.getElementById(id).style.display = 'none';
|
|
304
305
|
}
|
|
306
|
+
|
|
307
|
+
async function planView(file) {
|
|
308
|
+
try {
|
|
309
|
+
const normalizedFile = normalizePlanFile(file);
|
|
310
|
+
const planRes = await fetch('/api/plans/' + encodeURIComponent(normalizedFile));
|
|
311
|
+
const lastMod = planRes.headers.get('Last-Modified');
|
|
312
|
+
const resolvedPath = planRes.headers.get('X-Resolved-Path');
|
|
313
|
+
const raw = await planRes.text();
|
|
314
|
+
let title = normalizedFile;
|
|
315
|
+
let text = '';
|
|
316
|
+
|
|
317
|
+
if (normalizedFile.endsWith('.json')) {
|
|
318
|
+
// PRD JSON — format nicely
|
|
319
|
+
const plan = JSON.parse(raw);
|
|
320
|
+
title = plan.plan_summary || normalizedFile;
|
|
321
|
+
const items = (plan.missing_features || []).map((f, i) =>
|
|
322
|
+
(i + 1) + '. [' + f.id + '] ' + f.name + ' (' + (f.estimated_complexity || '?') + ', ' + (f.priority || '?') + ')' +
|
|
323
|
+
(f.depends_on?.length ? ' → depends on: ' + f.depends_on.join(', ') : '') +
|
|
324
|
+
'\n ' + (f.description || '').slice(0, 200) +
|
|
325
|
+
(f.acceptance_criteria?.length ? '\n Criteria: ' + f.acceptance_criteria.join('; ') : '')
|
|
326
|
+
).join('\n\n');
|
|
327
|
+
text = 'Project: ' + (plan.project || '?') +
|
|
328
|
+
'\nStrategy: ' + (plan.branch_strategy || 'parallel') +
|
|
329
|
+
'\nBranch: ' + (plan.feature_branch || 'per-item') +
|
|
330
|
+
'\nStatus: ' + (plan.status || 'active') +
|
|
331
|
+
'\nGenerated by: ' + (plan.generated_by || '?') + ' on ' + (plan.generated_at || '?') +
|
|
332
|
+
'\n\n--- Items (' + (plan.missing_features || []).length + ') ---\n\n' + items +
|
|
333
|
+
(plan.open_questions?.length ? '\n\n--- Open Questions ---\n\n' + plan.open_questions.map(q => '• ' + q).join('\n') : '');
|
|
334
|
+
} else {
|
|
335
|
+
// Markdown plan — show as-is
|
|
336
|
+
text = raw;
|
|
337
|
+
const titleMatch = raw.match(/^#\s+(?:Plan:\s*)?(.+)/m);
|
|
338
|
+
if (titleMatch) title = titleMatch[1];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Version badge for the modal title
|
|
342
|
+
const vMatch = normalizedFile.match(/-v(\d+)/);
|
|
343
|
+
const versionLabel = vMatch ? ' (v' + vMatch[1] + ')' : '';
|
|
344
|
+
|
|
345
|
+
// Determine plan type and status for action buttons
|
|
346
|
+
const isMdPlan = normalizedFile.endsWith('.md');
|
|
347
|
+
let planStatus = '';
|
|
348
|
+
try { if (normalizedFile.endsWith('.json')) planStatus = JSON.parse(raw).status || ''; } catch {}
|
|
349
|
+
const isActive = planStatus === 'approved' || planStatus === 'active';
|
|
350
|
+
const isPaused = planStatus === 'awaiting-approval' || planStatus === 'paused';
|
|
351
|
+
// Check if work is in progress for this plan
|
|
352
|
+
const wi = window._lastWorkItems || [];
|
|
353
|
+
const hasActiveWork = wi.some(w =>
|
|
354
|
+
(w.status === 'pending' || w.status === 'dispatched') &&
|
|
355
|
+
(w.planFile === normalizedFile || w.sourcePlan === normalizedFile ||
|
|
356
|
+
// For .md plans: any work item with a sourcePlan .json means the plan is being executed
|
|
357
|
+
(isMdPlan && w.sourcePlan && w.sourcePlan.endsWith('.json')))
|
|
358
|
+
);
|
|
359
|
+
const prdCompleted = wi.some(w => w.type === 'plan-to-prd' && w.status === 'done' && w.planFile === normalizedFile);
|
|
360
|
+
// Check if a PRD already exists for this plan (via plans list sourcePlan linkage)
|
|
361
|
+
const hasPrd = (window._lastStatus?.plans || []).some(p => p.sourcePlan === normalizedFile && p.format === 'prd');
|
|
362
|
+
const modalShowResume = isPaused;
|
|
363
|
+
const modalExecuteBtn = isMdPlan && !modalShowResume && !hasActiveWork && !prdCompleted && !hasPrd ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--green);font-weight:600" ' +
|
|
364
|
+
'onclick="planExecute(\'' + escHtml(normalizedFile) + '\',\'\',this)">Execute</button>' : '';
|
|
365
|
+
const modalCompletedLabel = prdCompleted && !hasActiveWork ? '<span style="font-size:10px;color:var(--green);font-weight:600">Completed</span>' : '';
|
|
366
|
+
const modalInProgressLabel = hasActiveWork ? '<span style="font-size:10px;color:var(--blue)">In Progress</span>' : '';
|
|
367
|
+
const isModalCompleted = planStatus === 'completed';
|
|
368
|
+
const modalPauseBtn = isActive && !isMdPlan && !isModalCompleted ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--yellow)" ' +
|
|
369
|
+
'onclick="planPause(\'' + escHtml(normalizedFile) + '\');closeModal()">Pause</button>' : '';
|
|
370
|
+
const modalResumeBtn = isPaused ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--green)" ' +
|
|
371
|
+
'onclick="planApprove(\'' + escHtml(normalizedFile) + '\');closeModal()">Resume</button>' : '';
|
|
372
|
+
|
|
373
|
+
const lastModLabel = lastMod ? '<div style="font-size:10px;color:var(--muted);font-weight:400;margin-top:2px">Last updated: ' + new Date(lastMod).toLocaleString() + '</div>' : '';
|
|
374
|
+
const actionBtns = '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px">' +
|
|
375
|
+
(modalCompletedLabel || '') + (modalInProgressLabel || '') + (modalExecuteBtn || '') + (modalPauseBtn || '') + (modalResumeBtn || '') +
|
|
376
|
+
' <button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--red)" ' +
|
|
377
|
+
'onclick="planDelete(\'' + escHtml(normalizedFile) + '\')">Delete</button>' +
|
|
378
|
+
'</div>';
|
|
379
|
+
document.getElementById('modal-title').innerHTML = escHtml(title) + (versionLabel ? ' <span style="font-size:11px;font-weight:700;padding:1px 6px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue)">' + escHtml(versionLabel) + '</span>' : '') + lastModLabel + actionBtns;
|
|
380
|
+
document.getElementById('modal-body').textContent = text;
|
|
381
|
+
document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
|
|
382
|
+
document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
|
|
383
|
+
_modalDocContext = { title, content: text, selection: '' };
|
|
384
|
+
_modalFilePath = resolvedPath || ((normalizedFile.endsWith('.json') ? 'prd/' : 'plans/') + normalizedFile); showModalQa();
|
|
385
|
+
// Clear notification badge when opening this document
|
|
386
|
+
const card = findCardForFile(_modalFilePath);
|
|
387
|
+
if (card) clearNotifBadge(card);
|
|
388
|
+
// steer btn removed — unified send
|
|
389
|
+
document.getElementById('modal').classList.add('open');
|
|
390
|
+
} catch (e) { console.error(e); }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function planApprove(file) {
|
|
394
|
+
try {
|
|
395
|
+
await fetch('/api/plans/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file }) });
|
|
396
|
+
showToast('cmd-toast', 'Plan approved — work will begin on next engine tick', true);
|
|
397
|
+
refreshPlans();
|
|
398
|
+
} catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function planDelete(file) {
|
|
402
|
+
if (!confirm('Delete plan "' + file + '"? This cannot be undone.')) return;
|
|
403
|
+
try {
|
|
404
|
+
const res = await fetch('/api/plans/delete', {
|
|
405
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
406
|
+
body: JSON.stringify({ file })
|
|
407
|
+
});
|
|
408
|
+
if (res.ok) {
|
|
409
|
+
closeModal();
|
|
410
|
+
showToast('cmd-toast', 'Plan deleted', true);
|
|
411
|
+
refreshPlans();
|
|
412
|
+
refresh();
|
|
413
|
+
} else {
|
|
414
|
+
const d = await res.json();
|
|
415
|
+
alert('Failed: ' + (d.error || 'unknown'));
|
|
416
|
+
}
|
|
417
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function planPause(file) {
|
|
421
|
+
try {
|
|
422
|
+
await fetch('/api/plans/pause', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file }) });
|
|
423
|
+
showToast('cmd-toast', 'Plan paused — no new items will be dispatched', true);
|
|
424
|
+
refreshPlans();
|
|
425
|
+
refresh();
|
|
426
|
+
} catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function planReject(file) {
|
|
430
|
+
if (!confirm('Reject this plan? It will not be executed.')) return;
|
|
431
|
+
const reason = prompt('Reason for rejection (optional):') || '';
|
|
432
|
+
try {
|
|
433
|
+
await fetch('/api/plans/reject', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file, reason }) });
|
|
434
|
+
showToast('cmd-toast', 'Plan rejected', true);
|
|
435
|
+
refreshPlans();
|
|
436
|
+
} catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function planDiscuss(file) {
|
|
440
|
+
try {
|
|
441
|
+
const res = await fetch('/api/plans/discuss', {
|
|
442
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
443
|
+
body: JSON.stringify({ file })
|
|
444
|
+
});
|
|
445
|
+
const data = await res.json();
|
|
446
|
+
if (!res.ok) throw new Error(data.error);
|
|
447
|
+
|
|
448
|
+
// Show the launch command in a modal
|
|
449
|
+
const content = `To discuss and revise this plan interactively, run this command in a terminal:\n\n` +
|
|
450
|
+
`━━━ Bash / Git Bash ━━━\n${data.command}\n\n` +
|
|
451
|
+
`━━━ PowerShell ━━━\n${data.psCommand}\n\n` +
|
|
452
|
+
`━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
|
|
453
|
+
`This launches an interactive Claude session with the plan pre-loaded.\n` +
|
|
454
|
+
`Chat naturally to review and refine. When you're satisfied, say "approve" and the session will write the approved plan back to disk.\n\n` +
|
|
455
|
+
`The engine will pick it up on the next tick and start dispatching work.`;
|
|
456
|
+
|
|
457
|
+
document.getElementById('modal-title').textContent = 'Discuss Plan: ' + file;
|
|
458
|
+
document.getElementById('modal-body').textContent = content;
|
|
459
|
+
document.getElementById('modal').classList.add('open');
|
|
460
|
+
} catch (e) {
|
|
461
|
+
showToast('cmd-toast', 'Error: ' + e.message, false);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function planOpenInDocChat(file) {
|
|
466
|
+
try {
|
|
467
|
+
const normalizedFile = normalizePlanFile(file);
|
|
468
|
+
const planRes = await fetch('/api/plans/' + encodeURIComponent(normalizedFile));
|
|
469
|
+
const resolvedPath = planRes.headers.get('X-Resolved-Path');
|
|
470
|
+
const raw = await planRes.text();
|
|
471
|
+
let title = normalizedFile;
|
|
472
|
+
let text = raw;
|
|
473
|
+
if (normalizedFile.endsWith('.json')) {
|
|
474
|
+
try { title = JSON.parse(raw).plan_summary || file; } catch {}
|
|
475
|
+
}
|
|
476
|
+
document.getElementById('modal-title').textContent = 'Edit: ' + title;
|
|
477
|
+
document.getElementById('modal-body').textContent = text;
|
|
478
|
+
document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
|
|
479
|
+
document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
|
|
480
|
+
_modalDocContext = { title: title, content: text, selection: '' };
|
|
481
|
+
_modalFilePath = resolvedPath || ((normalizedFile.endsWith('.json') ? 'prd/' : 'plans/') + normalizedFile); showModalQa();
|
|
482
|
+
const card = findCardForFile(_modalFilePath);
|
|
483
|
+
if (card) clearNotifBadge(card);
|
|
484
|
+
document.getElementById('modal').classList.add('open');
|
|
485
|
+
} catch (e) { alert('Error opening plan: ' + e.message); }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function planRegeneratePRD(source) {
|
|
489
|
+
if (!confirm('Reset pending/failed items to pick up plan changes?\n\nIn-progress and completed items won\'t be affected.')) return;
|
|
490
|
+
try {
|
|
491
|
+
const res = await fetch('/api/plans/regenerate', {
|
|
492
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
493
|
+
body: JSON.stringify({ source })
|
|
494
|
+
});
|
|
495
|
+
const d = await res.json();
|
|
496
|
+
if (res.ok && d.ok) {
|
|
497
|
+
refresh();
|
|
498
|
+
showToast('cmd-toast', 'Regenerated: ' + d.reset + ' reset, ' + d.kept + ' kept, ' + d.new + ' new', true);
|
|
499
|
+
} else {
|
|
500
|
+
alert('Failed: ' + (d.error || 'unknown'));
|
|
501
|
+
}
|
|
502
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function openVerifyGuide(file) {
|
|
506
|
+
try {
|
|
507
|
+
const normalizedFile = normalizePlanFile(file);
|
|
508
|
+
const content = await fetch('/api/plans/' + encodeURIComponent(normalizedFile)).then(r => r.text());
|
|
509
|
+
document.getElementById('modal-title').innerHTML = 'Manual Testing Guide' +
|
|
510
|
+
' <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;margin-left:8px;vertical-align:middle" onclick="openArchivedPrdModal()">Back</button>';
|
|
511
|
+
document.getElementById('modal-body').textContent = content;
|
|
512
|
+
document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
|
|
513
|
+
document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
|
|
514
|
+
_modalDocContext = { title: 'Manual Testing Guide', content, selection: '' };
|
|
515
|
+
_modalFilePath = 'prd/' + normalizedFile; showModalQa();
|
|
516
|
+
const card = findCardForFile(_modalFilePath);
|
|
517
|
+
if (card) clearNotifBadge(card);
|
|
518
|
+
document.getElementById('modal').classList.add('open');
|
|
519
|
+
} catch (e) { alert('Failed to load guide: ' + e.message); }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function triggerVerify(file) {
|
|
523
|
+
try {
|
|
524
|
+
const res = await fetch('/api/plans/trigger-verify', {
|
|
525
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
526
|
+
body: JSON.stringify({ file })
|
|
527
|
+
});
|
|
528
|
+
const d = await res.json();
|
|
529
|
+
if (res.ok && d.ok) {
|
|
530
|
+
closeModal();
|
|
531
|
+
refresh();
|
|
532
|
+
showToast('cmd-toast', d.verifyId ? 'Verify task ' + d.verifyId + ' created' : (d.message || 'Done'), true);
|
|
533
|
+
} else {
|
|
534
|
+
alert('Failed: ' + (d.error || 'unknown'));
|
|
535
|
+
}
|
|
536
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
537
|
+
}
|
|
@@ -467,3 +467,223 @@ function showArchivedPrdDetail(idx) {
|
|
|
467
467
|
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
468
468
|
document.getElementById('modal').classList.add('open');
|
|
469
469
|
}
|
|
470
|
+
|
|
471
|
+
async function prdItemEdit(source, itemId) {
|
|
472
|
+
const item = _prdItems.find(i => i.source === source && i.id === itemId);
|
|
473
|
+
if (!item) return;
|
|
474
|
+
|
|
475
|
+
// Look up work item and dispatch completion info
|
|
476
|
+
const wi = (window._lastWorkItems || []).find(w => w.id === itemId && w.sourcePlan === source);
|
|
477
|
+
const dispatch = window._lastStatus?.dispatch || {};
|
|
478
|
+
const completedEntry = (dispatch.completed || []).find(d =>
|
|
479
|
+
d.meta?.item?.id === itemId && d.meta?.item?.sourcePlan === source
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
// Build completion summary section
|
|
483
|
+
let completionHtml = '';
|
|
484
|
+
const isDone = item.status === 'done' || item.status === 'implemented' || item.status === 'complete' || item.status === 'in-pr'; // in-pr treated as done for backward compat
|
|
485
|
+
const isFailed = item.status === 'failed';
|
|
486
|
+
const isActive = item.status === 'in-progress' || item.status === 'dispatched';
|
|
487
|
+
|
|
488
|
+
if (isDone || isFailed || isActive) {
|
|
489
|
+
const agent = wi?.dispatched_to || completedEntry?.agent || '';
|
|
490
|
+
const completedAt = wi?.completedAt || completedEntry?.completed_at || '';
|
|
491
|
+
const summary = completedEntry?.resultSummary || '';
|
|
492
|
+
const prLinks = (item.prs || []).map(function(pr) {
|
|
493
|
+
return '<a href="' + escHtml(pr.url || '#') + '" target="_blank" rel="noopener" style="color:var(--green);text-decoration:underline">' + escHtml(pr.id) + '</a>';
|
|
494
|
+
}).join(', ');
|
|
495
|
+
|
|
496
|
+
const statusColor = isDone ? 'var(--green)' : isFailed ? 'var(--red)' : 'var(--blue)';
|
|
497
|
+
const statusLabel = isDone ? 'Completed' : isFailed ? 'Failed' : 'In Progress';
|
|
498
|
+
|
|
499
|
+
completionHtml = '<div style="background:var(--surface);border:1px solid var(--border);border-left:3px solid ' + statusColor + ';border-radius:4px;padding:10px 12px;margin-bottom:12px">' +
|
|
500
|
+
'<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">' +
|
|
501
|
+
'<span style="font-size:11px;font-weight:700;color:' + statusColor + '">' + statusLabel + '</span>' +
|
|
502
|
+
(agent ? '<span style="font-size:11px;color:var(--muted)">by ' + escHtml(agent) + '</span>' : '') +
|
|
503
|
+
(completedAt ? '<span style="font-size:10px;color:var(--muted)">' + escHtml(completedAt.slice(0, 16).replace('T', ' ')) + '</span>' : '') +
|
|
504
|
+
'</div>' +
|
|
505
|
+
(prLinks ? '<div style="font-size:11px;margin-bottom:6px">PR: ' + prLinks + '</div>' : '') +
|
|
506
|
+
(summary ? '<div style="font-size:12px;color:var(--text);line-height:1.5;white-space:pre-wrap;max-height:300px;overflow-y:auto">' + escHtml(summary) + '</div>' : '') +
|
|
507
|
+
(isFailed && completedEntry?.reason ? '<div style="font-size:11px;color:var(--red);margin-top:4px">' + escHtml(completedEntry.reason) + '</div>' : '') +
|
|
508
|
+
'</div>';
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const html = '<div style="padding:8px 0">' +
|
|
512
|
+
completionHtml +
|
|
513
|
+
'<label style="font-size:11px;color:var(--muted);display:block;margin-bottom:4px">Name</label>' +
|
|
514
|
+
'<input id="prd-edit-name" value="' + escHtml(item.name || '') + '" style="width:100%;padding:6px 10px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;margin-bottom:10px">' +
|
|
515
|
+
'<label style="font-size:11px;color:var(--muted);display:block;margin-bottom:4px">Description</label>' +
|
|
516
|
+
'<textarea id="prd-edit-desc" rows="4" style="width:100%;padding:6px 10px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px;resize:vertical;margin-bottom:10px">' + escHtml(item.description || '') + '</textarea>' +
|
|
517
|
+
'<div style="display:flex;gap:12px;margin-bottom:12px">' +
|
|
518
|
+
'<div><label style="font-size:11px;color:var(--muted);display:block;margin-bottom:4px">Priority</label>' +
|
|
519
|
+
'<select id="prd-edit-priority" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text)">' +
|
|
520
|
+
'<option value="high"' + (item.priority === 'high' ? ' selected' : '') + '>High</option>' +
|
|
521
|
+
'<option value="medium"' + (item.priority === 'medium' ? ' selected' : '') + '>Medium</option>' +
|
|
522
|
+
'<option value="low"' + (item.priority === 'low' ? ' selected' : '') + '>Low</option>' +
|
|
523
|
+
'</select></div>' +
|
|
524
|
+
'<div><label style="font-size:11px;color:var(--muted);display:block;margin-bottom:4px">Complexity</label>' +
|
|
525
|
+
'<select id="prd-edit-complexity" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text)">' +
|
|
526
|
+
'<option value="small"' + (item.complexity === 'small' ? ' selected' : '') + '>Small</option>' +
|
|
527
|
+
'<option value="medium"' + (item.complexity === 'medium' ? ' selected' : '') + '>Medium</option>' +
|
|
528
|
+
'<option value="large"' + (item.complexity === 'large' ? ' selected' : '') + '>Large</option>' +
|
|
529
|
+
'</select></div>' +
|
|
530
|
+
'</div>' +
|
|
531
|
+
'<div style="display:flex;gap:8px">' +
|
|
532
|
+
'<button class="plan-btn approve" onclick="prdItemSave(\'' + escHtml(source) + '\',\'' + escHtml(itemId) + '\')">Save</button>' +
|
|
533
|
+
'<button class="plan-btn" onclick="closeModal()">Cancel</button>' +
|
|
534
|
+
'<button class="plan-btn reject" style="margin-left:auto" onclick="prdItemRemove(\'' + escHtml(source) + '\',\'' + escHtml(itemId) + '\')">Remove Item</button>' +
|
|
535
|
+
'</div>' +
|
|
536
|
+
'</div>';
|
|
537
|
+
|
|
538
|
+
document.getElementById('modal-title').textContent = item.id + ' — ' + (item.name || '').slice(0, 60);
|
|
539
|
+
document.getElementById('modal-body').innerHTML = html;
|
|
540
|
+
document.getElementById('modal-body').style.fontFamily = '';
|
|
541
|
+
document.getElementById('modal-body').style.whiteSpace = '';
|
|
542
|
+
document.getElementById('modal').classList.add('open');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function prdItemSave(source, itemId) {
|
|
546
|
+
try {
|
|
547
|
+
const res = await fetch('/api/prd-items/update', {
|
|
548
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
549
|
+
body: JSON.stringify({
|
|
550
|
+
source, itemId,
|
|
551
|
+
name: document.getElementById('prd-edit-name').value,
|
|
552
|
+
description: document.getElementById('prd-edit-desc').value,
|
|
553
|
+
priority: document.getElementById('prd-edit-priority').value,
|
|
554
|
+
estimated_complexity: document.getElementById('prd-edit-complexity').value,
|
|
555
|
+
})
|
|
556
|
+
});
|
|
557
|
+
if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', 'Item updated', true); }
|
|
558
|
+
else { const d = await res.json(); alert('Failed: ' + (d.error || 'unknown')); }
|
|
559
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function prdItemRemove(source, itemId) {
|
|
563
|
+
if (!confirm('Remove item ' + itemId + '? This also cancels any pending work item.')) return;
|
|
564
|
+
try {
|
|
565
|
+
const res = await fetch('/api/prd-items/remove', {
|
|
566
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
567
|
+
body: JSON.stringify({ source, itemId })
|
|
568
|
+
});
|
|
569
|
+
if (res.ok) { closeModal(); refresh(); showToast('cmd-toast', 'Item removed', true); }
|
|
570
|
+
else { const d = await res.json(); alert('Failed: ' + (d.error || 'unknown')); }
|
|
571
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function prdItemRequeue(workItemId, source) {
|
|
575
|
+
setPrdRequeueState(workItemId, { status: 'pending', message: '' });
|
|
576
|
+
rerenderPrdFromCache();
|
|
577
|
+
try {
|
|
578
|
+
const res = await fetch('/api/work-items/retry', {
|
|
579
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
580
|
+
body: JSON.stringify({ id: workItemId, source })
|
|
581
|
+
});
|
|
582
|
+
if (res.ok) {
|
|
583
|
+
setPrdRequeueState(workItemId, { status: 'queued', until: Date.now() + 10000 });
|
|
584
|
+
rerenderPrdFromCache();
|
|
585
|
+
wakeEngine();
|
|
586
|
+
refresh();
|
|
587
|
+
showToast('cmd-toast', workItemId + ' requeued', true);
|
|
588
|
+
} else {
|
|
589
|
+
const d = await res.json();
|
|
590
|
+
const msg = d.error || 'unknown';
|
|
591
|
+
setPrdRequeueState(workItemId, { status: 'error', message: msg, until: Date.now() + 10000 });
|
|
592
|
+
rerenderPrdFromCache();
|
|
593
|
+
alert('Failed: ' + msg);
|
|
594
|
+
}
|
|
595
|
+
} catch (e) {
|
|
596
|
+
setPrdRequeueState(workItemId, { status: 'error', message: e.message, until: Date.now() + 10000 });
|
|
597
|
+
rerenderPrdFromCache();
|
|
598
|
+
alert('Error: ' + e.message);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function prdRegenerate(prdFile) {
|
|
603
|
+
if (!confirm('This PRD is stale because the source plan changed.\n\nRegenerate now from the latest plan?\n\nThis resets PRD status to awaiting-approval and queues a fresh plan-to-prd conversion.')) return;
|
|
604
|
+
try {
|
|
605
|
+
const res = await fetch('/api/prd/regenerate', {
|
|
606
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
607
|
+
body: JSON.stringify({ file: prdFile })
|
|
608
|
+
});
|
|
609
|
+
const d = await res.json();
|
|
610
|
+
if (res.ok) {
|
|
611
|
+
showToast('cmd-toast', 'PRD regeneration queued', true);
|
|
612
|
+
refresh();
|
|
613
|
+
} else {
|
|
614
|
+
alert('Failed: ' + (d.error || 'unknown'));
|
|
615
|
+
}
|
|
616
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function openArchive(i) {
|
|
620
|
+
const a = archivedPrds[i];
|
|
621
|
+
if (!a) return;
|
|
622
|
+
|
|
623
|
+
document.getElementById('modal-title').textContent = 'Archived PRD — ' + a.version + ' (' + a.total + ' items)';
|
|
624
|
+
|
|
625
|
+
let html = '<div style="font-family:\'Segoe UI\',system-ui,sans-serif;white-space:normal">';
|
|
626
|
+
|
|
627
|
+
// Summary
|
|
628
|
+
if (a.summary) {
|
|
629
|
+
html += '<div class="archive-detail-section"><h4>Summary</h4><p style="font-size:12px;color:var(--muted);line-height:1.6">' + escHtml(a.summary) + '</p></div>';
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Existing features
|
|
633
|
+
if (a.existing_features.length) {
|
|
634
|
+
html += '<div class="archive-detail-section"><h4>Existing Features (' + a.existing_features.length + ')</h4>';
|
|
635
|
+
a.existing_features.forEach(f => {
|
|
636
|
+
html += '<div class="archive-feature">' +
|
|
637
|
+
'<span class="feat-id">' + escHtml(f.id) + '</span>' +
|
|
638
|
+
'<div class="feat-name">' + escHtml(f.name) + '</div>' +
|
|
639
|
+
'<div class="feat-desc">' + escHtml(f.description || '') + '</div>' +
|
|
640
|
+
'<div class="feat-meta">' +
|
|
641
|
+
(f.agent ? '<span>Agent: ' + escHtml(f.agent) + '</span>' : '') +
|
|
642
|
+
(f.status ? '<span>Status: ' + escHtml(f.status) + '</span>' : '') +
|
|
643
|
+
'</div>' +
|
|
644
|
+
'</div>';
|
|
645
|
+
});
|
|
646
|
+
html += '</div>';
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Missing features
|
|
650
|
+
if (a.missing_features.length) {
|
|
651
|
+
html += '<div class="archive-detail-section"><h4>Gap Items (' + a.missing_features.length + ')</h4>';
|
|
652
|
+
a.missing_features.forEach(f => {
|
|
653
|
+
const pClass = f.priority === 'high' ? 'high' : f.priority === 'medium' ? 'medium' : 'low';
|
|
654
|
+
html += '<div class="archive-feature">' +
|
|
655
|
+
'<span class="feat-id">' + escHtml(f.id) + '</span> ' +
|
|
656
|
+
'<span class="prd-item-priority ' + pClass + '">' + escHtml(f.priority || '') + '</span>' +
|
|
657
|
+
(f.status ? ' <span class="pr-badge ' + (f.status === 'complete' || f.status === 'done' || f.status === 'in-pr' ? 'approved' : 'draft') + '" style="font-size:9px">' + escHtml(f.status === 'in-pr' ? 'done' : f.status) + '</span>' : '') +
|
|
658
|
+
'<div class="feat-name">' + escHtml(f.name) + '</div>' +
|
|
659
|
+
'<div class="feat-desc">' + escHtml(f.description || '') + '</div>' +
|
|
660
|
+
(f.rationale ? '<div class="feat-desc" style="margin-top:4px;color:var(--yellow)">Rationale: ' + escHtml(f.rationale) + '</div>' : '') +
|
|
661
|
+
'<div class="feat-meta">' +
|
|
662
|
+
(f.estimated_complexity ? '<span>Complexity: ' + escHtml(f.estimated_complexity) + '</span>' : '') +
|
|
663
|
+
(f.affected_areas ? '<span>Areas: ' + escHtml(f.affected_areas.join(', ')) + '</span>' : '') +
|
|
664
|
+
'</div>' +
|
|
665
|
+
'</div>';
|
|
666
|
+
});
|
|
667
|
+
html += '</div>';
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Open questions
|
|
671
|
+
if (a.open_questions.length) {
|
|
672
|
+
html += '<div class="archive-detail-section"><h4>Open Questions (' + a.open_questions.length + ')</h4>';
|
|
673
|
+
a.open_questions.forEach(q => {
|
|
674
|
+
html += '<div class="archive-question">' +
|
|
675
|
+
'<span class="q-id">' + escHtml(q.id) + '</span>' +
|
|
676
|
+
'<div class="q-text">' + escHtml(q.question) + '</div>' +
|
|
677
|
+
(q.context ? '<div class="q-context">' + escHtml(q.context) + '</div>' : '') +
|
|
678
|
+
'</div>';
|
|
679
|
+
});
|
|
680
|
+
html += '</div>';
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
html += '</div>';
|
|
684
|
+
|
|
685
|
+
document.getElementById('modal-body').innerHTML = html;
|
|
686
|
+
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
687
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
688
|
+
document.getElementById('modal').classList.add('open');
|
|
689
|
+
}
|
|
@@ -198,7 +198,7 @@ async function retryWorkItem(id, source) {
|
|
|
198
198
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
199
199
|
body: JSON.stringify({ id, source: source || undefined })
|
|
200
200
|
});
|
|
201
|
-
if (res.ok) { refresh(); } else {
|
|
201
|
+
if (res.ok) { wakeEngine(); refresh(); } else {
|
|
202
202
|
const d = await res.json();
|
|
203
203
|
alert('Retry failed: ' + (d.error || 'unknown'));
|
|
204
204
|
}
|
package/dashboard/js/settings.js
CHANGED
|
@@ -133,3 +133,23 @@ async function saveSettings() {
|
|
|
133
133
|
status.style.color = 'var(--red)';
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
|
|
137
|
+
async function addProject() {
|
|
138
|
+
try {
|
|
139
|
+
// Open folder picker
|
|
140
|
+
const browseRes = await fetch('/api/projects/browse', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
|
141
|
+
const browseData = await browseRes.json();
|
|
142
|
+
if (browseData.cancelled || !browseData.path) return;
|
|
143
|
+
|
|
144
|
+
// Add the project
|
|
145
|
+
const addRes = await fetch('/api/projects/add', {
|
|
146
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
147
|
+
body: JSON.stringify({ path: browseData.path })
|
|
148
|
+
});
|
|
149
|
+
const addData = await addRes.json();
|
|
150
|
+
if (!addRes.ok) { alert('Failed: ' + (addData.error || 'unknown')); return; }
|
|
151
|
+
|
|
152
|
+
showToast('cmd-toast', 'Project "' + addData.name + '" added — restart engine to pick it up', true);
|
|
153
|
+
refresh();
|
|
154
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
155
|
+
}
|
package/dashboard/js/utils.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// dashboard/js/utils.js — Utility functions extracted from dashboard.html
|
|
2
2
|
|
|
3
|
+
// Signal the engine to tick immediately (pick up new work without waiting 60s)
|
|
4
|
+
function wakeEngine() { fetch('/api/engine/wakeup', { method: 'POST' }).catch(() => {}); }
|
|
5
|
+
|
|
3
6
|
function escHtml(s) {
|
|
4
7
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
5
8
|
}
|
package/dashboard-build.js
CHANGED
|
@@ -42,10 +42,11 @@ function buildDashboardHtml() {
|
|
|
42
42
|
jsHtml += `\n// ─── ${f}.js ────────────────────────────────────────\n${content}\n`;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Use function replacer to avoid $ special patterns in String.replace
|
|
45
46
|
return layout
|
|
46
|
-
.replace('/* __CSS__ */', css)
|
|
47
|
-
.replace('<!-- __PAGES__ -->', pageHtml)
|
|
48
|
-
.replace('/* __JS__ */', jsHtml);
|
|
47
|
+
.replace('/* __CSS__ */', () => css)
|
|
48
|
+
.replace('<!-- __PAGES__ -->', () => pageHtml)
|
|
49
|
+
.replace('/* __JS__ */', () => jsHtml);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
module.exports = { buildDashboardHtml };
|
package/dashboard.js
CHANGED
|
@@ -54,7 +54,7 @@ function buildDashboardHtml() {
|
|
|
54
54
|
const dashDir = path.join(MINIONS_DIR, 'dashboard');
|
|
55
55
|
const layoutPath = path.join(dashDir, 'layout.html');
|
|
56
56
|
|
|
57
|
-
// Fall back to monolith if
|
|
57
|
+
// Fall back to monolith if fragments don't exist
|
|
58
58
|
if (!fs.existsSync(layoutPath)) {
|
|
59
59
|
return safeRead(path.join(MINIONS_DIR, 'dashboard.html')) || '';
|
|
60
60
|
}
|
|
@@ -87,9 +87,9 @@ function buildDashboardHtml() {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
return layout
|
|
90
|
-
.replace('/* __CSS__ */', css)
|
|
91
|
-
.replace('<!-- __PAGES__ -->', pageHtml)
|
|
92
|
-
.replace('/* __JS__ */', jsHtml);
|
|
90
|
+
.replace('/* __CSS__ */', () => css)
|
|
91
|
+
.replace('<!-- __PAGES__ -->', () => pageHtml)
|
|
92
|
+
.replace('/* __JS__ */', () => jsHtml);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
const HTML_RAW = buildDashboardHtml();
|
package/package.json
CHANGED