@yemi33/minions 0.1.12 → 0.1.14
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 +59 -0
- package/dashboard/js/command-center.js +377 -0
- package/dashboard/js/command-history.js +70 -0
- package/dashboard/js/command-input.js +268 -0
- package/dashboard/js/command-parser.js +129 -0
- package/dashboard/js/detail-panel.js +98 -0
- package/dashboard/js/live-stream.js +69 -0
- package/dashboard/js/modal-qa.js +309 -0
- package/dashboard/js/modal.js +131 -0
- package/dashboard/js/refresh.js +59 -0
- package/dashboard/js/render-agents.js +44 -0
- package/dashboard/js/render-dispatch.js +171 -0
- package/dashboard/js/render-inbox.js +163 -0
- package/dashboard/js/render-kb.js +125 -0
- package/dashboard/js/render-other.js +181 -0
- package/dashboard/js/render-plans.js +536 -0
- package/dashboard/js/render-prd.js +688 -0
- package/dashboard/js/render-prs.js +94 -0
- package/dashboard/js/render-schedules.js +158 -0
- package/dashboard/js/render-skills.js +89 -0
- package/dashboard/js/render-work-items.js +219 -0
- package/dashboard/js/settings.js +155 -0
- package/dashboard/js/state.js +84 -0
- package/dashboard/js/utils.js +39 -0
- package/dashboard/layout.html +123 -0
- package/dashboard/pages/engine.html +12 -0
- package/dashboard/pages/home.html +31 -0
- package/dashboard/pages/inbox.html +17 -0
- package/dashboard/pages/plans.html +4 -0
- package/dashboard/pages/prd.html +5 -0
- package/dashboard/pages/prs.html +4 -0
- package/dashboard/pages/schedule.html +10 -0
- package/dashboard/pages/work.html +5 -0
- package/dashboard/styles.css +598 -0
- package/dashboard-build.js +52 -0
- package/dashboard.js +44 -1
- package/package.json +1 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// dashboard/js/render-dispatch.js — Engine status, dispatch, and log rendering extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
function renderEngineStatus(engine) {
|
|
4
|
+
const badge = document.getElementById('engine-badge');
|
|
5
|
+
let state = engine?.state || 'stopped';
|
|
6
|
+
let staleMs = 0;
|
|
7
|
+
|
|
8
|
+
// Detect stale engine — says running but heartbeat is old (>2 min)
|
|
9
|
+
if (state === 'running' && engine?.heartbeat) {
|
|
10
|
+
staleMs = Date.now() - engine.heartbeat;
|
|
11
|
+
if (staleMs > 120000) {
|
|
12
|
+
state = 'stale';
|
|
13
|
+
}
|
|
14
|
+
} else if (state === 'running' && !engine?.heartbeat) {
|
|
15
|
+
// Running but no heartbeat yet — engine just started or old version
|
|
16
|
+
state = 'running';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
badge.className = 'engine-badge ' + (state === 'stale' ? 'stopped' : state);
|
|
20
|
+
badge.textContent = state === 'stale' ? 'STALE' : state.toUpperCase();
|
|
21
|
+
badge.title = state === 'stale'
|
|
22
|
+
? 'Engine claims running but heartbeat is stale (>2min). It may have crashed. Run: node engine.js start'
|
|
23
|
+
: state === 'stopped' ? 'Engine is stopped. Run: node engine.js start' : '';
|
|
24
|
+
renderEngineAlert(state, staleMs);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderEngineAlert(state, staleMs) {
|
|
28
|
+
const el = document.getElementById('engine-alert');
|
|
29
|
+
if (!el) return;
|
|
30
|
+
if (state !== 'stale') {
|
|
31
|
+
el.style.display = 'none';
|
|
32
|
+
el.innerHTML = '';
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const mins = Math.max(1, Math.round(staleMs / 60000));
|
|
36
|
+
el.innerHTML =
|
|
37
|
+
'<span class="engine-alert-msg">⚠️ Engine heartbeat is stale (' + mins + 'm old). Dispatch may be stuck.</span>' +
|
|
38
|
+
'<span class="engine-alert-action" id="engine-alert-restart">Restart engine</span>';
|
|
39
|
+
document.getElementById('engine-alert-restart').onclick = async function() {
|
|
40
|
+
this.classList.add('clicked');
|
|
41
|
+
this.textContent = 'Restarting...';
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch('/api/engine/restart', { method: 'POST' });
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
if (data.ok) {
|
|
46
|
+
this.textContent = 'Restarted (PID ' + data.pid + ')';
|
|
47
|
+
setTimeout(() => refresh(), 3000);
|
|
48
|
+
} else {
|
|
49
|
+
this.textContent = 'Failed: ' + (data.error || 'unknown');
|
|
50
|
+
this.classList.remove('clicked');
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
this.textContent = 'Failed: ' + e.message;
|
|
54
|
+
this.classList.remove('clicked');
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
el.style.display = 'flex';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renderDispatch(dispatch) {
|
|
61
|
+
if (!dispatch) return;
|
|
62
|
+
|
|
63
|
+
// Stats
|
|
64
|
+
const stats = document.getElementById('dispatch-stats');
|
|
65
|
+
stats.innerHTML =
|
|
66
|
+
'<div class="dispatch-stat"><div class="dispatch-stat-num yellow">' + (dispatch.active || []).length + '</div><div class="dispatch-stat-label">Active</div></div>' +
|
|
67
|
+
'<div class="dispatch-stat"><div class="dispatch-stat-num blue">' + (dispatch.pending || []).length + '</div><div class="dispatch-stat-label">Pending</div></div>' +
|
|
68
|
+
'<div class="dispatch-stat"><div class="dispatch-stat-num green">' + (dispatch.completed || []).length + '</div><div class="dispatch-stat-label">Completed</div></div>';
|
|
69
|
+
|
|
70
|
+
// Active
|
|
71
|
+
const activeEl = document.getElementById('dispatch-active');
|
|
72
|
+
if ((dispatch.active || []).length > 0) {
|
|
73
|
+
activeEl.innerHTML = '<div style="font-size:11px;color:var(--green);margin-bottom:6px;font-weight:600">ACTIVE</div><div class="dispatch-list">' +
|
|
74
|
+
dispatch.active.map(d =>
|
|
75
|
+
'<div class="dispatch-item">' +
|
|
76
|
+
'<span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span>' +
|
|
77
|
+
'<span class="dispatch-agent">' + escHtml(d.agentName || d.agent || '') + '</span>' +
|
|
78
|
+
'<span class="dispatch-task" title="' + escHtml(d.task || '') + '">' + escHtml(d.task || '') + '</span>' +
|
|
79
|
+
'<span class="dispatch-time">' + shortTime(d.started_at) + '</span>' +
|
|
80
|
+
'</div>'
|
|
81
|
+
).join('') + '</div>';
|
|
82
|
+
} else {
|
|
83
|
+
activeEl.innerHTML = '<div style="color:var(--muted);font-size:11px;margin-bottom:8px">No active dispatches</div>';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Pending
|
|
87
|
+
const pendingEl = document.getElementById('dispatch-pending');
|
|
88
|
+
if ((dispatch.pending || []).length > 0) {
|
|
89
|
+
pendingEl.innerHTML = '<div style="font-size:11px;color:var(--yellow);margin:8px 0 6px;font-weight:600">PENDING</div><div class="dispatch-list">' +
|
|
90
|
+
dispatch.pending.map(d =>
|
|
91
|
+
'<div class="dispatch-item">' +
|
|
92
|
+
'<span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span>' +
|
|
93
|
+
'<span class="dispatch-agent">' + escHtml(d.agentName || d.agent || '') + '</span>' +
|
|
94
|
+
'<span class="dispatch-task" title="' + escHtml(d.task || '') + '">' + escHtml(d.task || '') + '</span>' +
|
|
95
|
+
(d.skipReason ? '<span style="font-size:9px;color:var(--muted);margin-left:6px" title="' + escHtml(d.skipReason) + '">' + escHtml(d.skipReason.replace(/_/g, ' ')) + '</span>' : '') +
|
|
96
|
+
'</div>'
|
|
97
|
+
).join('') + '</div>';
|
|
98
|
+
} else {
|
|
99
|
+
pendingEl.innerHTML = '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Completed
|
|
103
|
+
const completedEl = document.getElementById('completed-content');
|
|
104
|
+
const completedCount = document.getElementById('completed-count');
|
|
105
|
+
const completed = (dispatch.completed || []).slice().reverse();
|
|
106
|
+
completedCount.textContent = completed.length;
|
|
107
|
+
|
|
108
|
+
if (completed.length > 0) {
|
|
109
|
+
completedEl.innerHTML = '<table class="pr-table"><thead><tr><th>ID</th><th>Type</th><th>Agent</th><th>Task</th><th>Result</th><th>Completed</th></tr></thead><tbody>' +
|
|
110
|
+
completed.map(d => {
|
|
111
|
+
const isError = d.result === 'error';
|
|
112
|
+
const agentId = (d.agent || '').toLowerCase();
|
|
113
|
+
const errorBtn = isError
|
|
114
|
+
? ' <button class="error-details-btn" data-agent="' + escHtml(agentId) + '" data-reason="' + escHtml(d.reason || 'No reason recorded') + '" data-task="' + escHtml((d.task || '').slice(0, 100)) + '" onclick="showErrorDetails(this.dataset.agent, this.dataset.reason, this.dataset.task)" title="View error details">details</button>'
|
|
115
|
+
: '';
|
|
116
|
+
return '<tr>' +
|
|
117
|
+
'<td style="font-family:Consolas;font-size:10px" title="' + escHtml(d.id || '') + '">' + escHtml(d.id || '') + '</td>' +
|
|
118
|
+
'<td><span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span></td>' +
|
|
119
|
+
'<td>' + escHtml(d.agentName || d.agent || '') + '</td>' +
|
|
120
|
+
'<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml((d.task || '').slice(0, 60)) + '</td>' +
|
|
121
|
+
'<td style="color:' + (d.result === 'success' ? 'var(--green)' : 'var(--red)') + '">' + escHtml(d.result || '') + errorBtn + '</td>' +
|
|
122
|
+
'<td class="pr-date">' + shortTime(d.completed_at) + '</td>' +
|
|
123
|
+
'</tr>';
|
|
124
|
+
}).join('') + '</tbody></table>';
|
|
125
|
+
} else {
|
|
126
|
+
completedEl.innerHTML = '<p class="empty">No completed dispatches yet.</p>';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderEngineLog(log) {
|
|
131
|
+
const el = document.getElementById('engine-log');
|
|
132
|
+
if (!log || log.length === 0) {
|
|
133
|
+
el.innerHTML = '<div class="empty">No log entries yet.</div>';
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
el.innerHTML = log.slice().reverse().map(e =>
|
|
137
|
+
'<div class="log-entry">' +
|
|
138
|
+
'<span class="log-ts">' + shortTime(e.timestamp) + '</span> ' +
|
|
139
|
+
'<span class="log-level-' + (e.level || 'info') + '">[' + (e.level || 'info') + ']</span> ' +
|
|
140
|
+
escHtml(e.message || '') +
|
|
141
|
+
'</div>'
|
|
142
|
+
).join('');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function shortTime(t) {
|
|
146
|
+
if (!t) return '';
|
|
147
|
+
try { return new Date(t).toLocaleTimeString(); } catch { return t; }
|
|
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
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// render-inbox.js — Inbox and notes rendering functions extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
function renderInbox(inbox) {
|
|
4
|
+
inboxData = inbox;
|
|
5
|
+
const list = document.getElementById('inbox-list');
|
|
6
|
+
const count = document.getElementById('inbox-count');
|
|
7
|
+
count.textContent = inbox.length;
|
|
8
|
+
if (!inbox.length) { list.innerHTML = '<p class="empty">No messages yet.</p>'; return; }
|
|
9
|
+
list.innerHTML = inbox.map((item, i) => `
|
|
10
|
+
<div class="inbox-item" data-file="notes/inbox/${escHtml(item.name)}">
|
|
11
|
+
<div class="inbox-name" onclick="openModal(${i})" style="cursor:pointer">
|
|
12
|
+
<span>${escHtml(item.name)}</span><span>${item.age}</span>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="inbox-preview" onclick="openModal(${i})" style="cursor:pointer">${escHtml(item.content.slice(0,200))}</div>
|
|
15
|
+
<div style="display:flex;gap:6px;margin-top:6px;align-items:center">
|
|
16
|
+
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="event.stopPropagation();promoteToKB('${escHtml(item.name)}')">Add to Knowledge Base</button>
|
|
17
|
+
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="event.stopPropagation();openInboxInExplorer('${escHtml(item.name)}')">Open in Explorer</button>
|
|
18
|
+
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red)" onclick="event.stopPropagation();deleteInboxItem('${escHtml(item.name)}')">Delete</button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
`).join('');
|
|
22
|
+
restoreNotifBadges();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function promoteToKB(name) {
|
|
26
|
+
const categories = [
|
|
27
|
+
{ id: 'architecture', label: 'Architecture' },
|
|
28
|
+
{ id: 'conventions', label: 'Conventions' },
|
|
29
|
+
{ id: 'project-notes', label: 'Project Notes' },
|
|
30
|
+
{ id: 'build-reports', label: 'Build Reports' },
|
|
31
|
+
{ id: 'reviews', label: 'Reviews' },
|
|
32
|
+
];
|
|
33
|
+
const picker = '<div style="padding:16px 20px">' +
|
|
34
|
+
'<p style="font-size:13px;color:var(--text);margin-bottom:12px">Choose a category for <strong>' + escHtml(name) + '</strong>:</p>' +
|
|
35
|
+
'<div style="display:flex;flex-direction:column;gap:8px">' +
|
|
36
|
+
categories.map(c =>
|
|
37
|
+
'<button class="pr-pager-btn" style="font-size:12px;padding:8px 16px;text-align:left" onclick="doPromoteToKB(\'' + escHtml(name) + '\',\'' + c.id + '\')">' + c.label + '</button>'
|
|
38
|
+
).join('') +
|
|
39
|
+
'</div></div>';
|
|
40
|
+
document.getElementById('modal-title').textContent = 'Add to Knowledge Base';
|
|
41
|
+
document.getElementById('modal-body').innerHTML = picker;
|
|
42
|
+
document.getElementById('modal').classList.add('open');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderNotes(notes) {
|
|
46
|
+
const el = document.getElementById('notes-list');
|
|
47
|
+
const content = typeof notes === 'object' ? notes.content : notes;
|
|
48
|
+
const updatedAt = typeof notes === 'object' ? notes.updatedAt : null;
|
|
49
|
+
|
|
50
|
+
// Show last updated timestamp
|
|
51
|
+
const updatedEl = document.getElementById('notes-updated');
|
|
52
|
+
if (updatedEl && updatedAt) {
|
|
53
|
+
const d = new Date(updatedAt);
|
|
54
|
+
updatedEl.textContent = 'updated ' + d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!content || !content.trim()) { el.innerHTML = '<p class="empty">No team notes yet.</p>'; return; }
|
|
58
|
+
el.innerHTML = '<div class="notes-preview" onclick="openNotesModal()" title="Click to expand">' + escHtml(content) + '</div>';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function openNotesModal() {
|
|
62
|
+
const preview = document.querySelector('.notes-preview');
|
|
63
|
+
if (!preview) return;
|
|
64
|
+
const content = preview.textContent;
|
|
65
|
+
document.getElementById('modal-title').textContent = 'Team Notes';
|
|
66
|
+
document.getElementById('modal-body').textContent = content;
|
|
67
|
+
document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
|
|
68
|
+
document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
|
|
69
|
+
_modalDocContext = { title: 'Team Notes', content, selection: '' };
|
|
70
|
+
_modalEditable = 'notes.md';
|
|
71
|
+
_modalFilePath = 'notes.md'; showModalQa();
|
|
72
|
+
document.getElementById('modal-edit-btn').style.display = '';
|
|
73
|
+
// steer btn removed — unified send
|
|
74
|
+
document.getElementById('modal').classList.add('open');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function modalToggleEdit() {
|
|
78
|
+
const body = document.getElementById('modal-body');
|
|
79
|
+
body.contentEditable = 'true';
|
|
80
|
+
body.style.border = '1px solid var(--blue)';
|
|
81
|
+
body.style.borderRadius = '4px';
|
|
82
|
+
body.style.padding = '12px';
|
|
83
|
+
body.style.outline = 'none';
|
|
84
|
+
body.focus();
|
|
85
|
+
document.getElementById('modal-edit-btn').style.display = 'none';
|
|
86
|
+
document.getElementById('modal-save-btn').style.display = '';
|
|
87
|
+
document.getElementById('modal-cancel-edit-btn').style.display = '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function modalSaveEdit() {
|
|
91
|
+
if (!_modalEditable) return;
|
|
92
|
+
const body = document.getElementById('modal-body');
|
|
93
|
+
const content = body.innerText;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch('/api/notes-save', {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({ file: _modalEditable, content }),
|
|
100
|
+
});
|
|
101
|
+
const data = await res.json();
|
|
102
|
+
if (!res.ok) throw new Error(data.error || 'Save failed');
|
|
103
|
+
|
|
104
|
+
body.contentEditable = 'false';
|
|
105
|
+
body.style.border = '';
|
|
106
|
+
body.style.padding = '';
|
|
107
|
+
document.getElementById('modal-edit-btn').style.display = '';
|
|
108
|
+
document.getElementById('modal-save-btn').style.display = 'none';
|
|
109
|
+
document.getElementById('modal-cancel-edit-btn').style.display = 'none';
|
|
110
|
+
_modalDocContext.content = content;
|
|
111
|
+
showToast('cmd-toast', 'Team Notes saved', true);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
showToast('cmd-toast', 'Error: ' + e.message, false);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function modalCancelEdit() {
|
|
118
|
+
const body = document.getElementById('modal-body');
|
|
119
|
+
body.contentEditable = 'false';
|
|
120
|
+
body.textContent = _modalDocContext.content; // revert
|
|
121
|
+
body.style.border = '';
|
|
122
|
+
body.style.padding = '';
|
|
123
|
+
document.getElementById('modal-edit-btn').style.display = '';
|
|
124
|
+
document.getElementById('modal-save-btn').style.display = 'none';
|
|
125
|
+
document.getElementById('modal-cancel-edit-btn').style.display = 'none';
|
|
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
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// render-kb.js — Knowledge base rendering functions extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
let _kbActiveTab = 'all';
|
|
4
|
+
|
|
5
|
+
async function refreshKnowledgeBase() {
|
|
6
|
+
try {
|
|
7
|
+
_kbData = await fetch('/api/knowledge').then(r => r.json());
|
|
8
|
+
renderKnowledgeBase();
|
|
9
|
+
} catch {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function renderKnowledgeBase() {
|
|
13
|
+
const tabsEl = document.getElementById('kb-tabs');
|
|
14
|
+
const listEl = document.getElementById('kb-list');
|
|
15
|
+
const countEl = document.getElementById('kb-count');
|
|
16
|
+
|
|
17
|
+
// Count total (skip non-array keys like lastSwept)
|
|
18
|
+
let total = 0;
|
|
19
|
+
for (const [k, v] of Object.entries(_kbData)) if (Array.isArray(v)) total += v.length;
|
|
20
|
+
countEl.textContent = total;
|
|
21
|
+
|
|
22
|
+
// Last swept timestamp
|
|
23
|
+
const sweptEl = document.getElementById('kb-swept-time');
|
|
24
|
+
if (sweptEl) sweptEl.textContent = _kbData.lastSwept ? 'swept ' + timeSinceStr(new Date(_kbData.lastSwept)) : '';
|
|
25
|
+
|
|
26
|
+
if (total === 0) {
|
|
27
|
+
tabsEl.innerHTML = '';
|
|
28
|
+
listEl.innerHTML = '<p class="empty">No knowledge entries yet. Notes are classified here after consolidation.</p>';
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Render tabs
|
|
33
|
+
let tabsHtml = '<button class="kb-tab ' + (_kbActiveTab === 'all' ? 'active' : '') + '" onclick="kbSetTab(\'all\')">All <span class="badge">' + total + '</span></button>';
|
|
34
|
+
for (const [cat, items] of Object.entries(_kbData)) {
|
|
35
|
+
if (!Array.isArray(items) || items.length === 0) continue;
|
|
36
|
+
const label = KB_CAT_LABELS[cat] || cat;
|
|
37
|
+
tabsHtml += '<button class="kb-tab ' + (_kbActiveTab === cat ? 'active' : '') + '" onclick="kbSetTab(\'' + cat + '\')">' + label + ' <span class="badge">' + items.length + '</span></button>';
|
|
38
|
+
}
|
|
39
|
+
tabsEl.innerHTML = tabsHtml;
|
|
40
|
+
|
|
41
|
+
// Collect items for active tab
|
|
42
|
+
let items = [];
|
|
43
|
+
if (_kbActiveTab === 'all') {
|
|
44
|
+
for (const [cat, catItems] of Object.entries(_kbData)) {
|
|
45
|
+
if (!Array.isArray(catItems)) continue;
|
|
46
|
+
for (const item of catItems) items.push({ ...item, category: cat });
|
|
47
|
+
}
|
|
48
|
+
items.sort((a, b) => b.date.localeCompare(a.date));
|
|
49
|
+
} else {
|
|
50
|
+
items = (_kbData[_kbActiveTab] || []).map(i => ({ ...i, category: _kbActiveTab }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (items.length === 0) {
|
|
54
|
+
listEl.innerHTML = '<p class="empty">No entries in this category.</p>';
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
listEl.innerHTML = items.slice(0, 50).map(item => {
|
|
59
|
+
const icon = KB_CAT_ICONS[item.category] || '\u{1F4C4}';
|
|
60
|
+
const label = KB_CAT_LABELS[item.category] || item.category;
|
|
61
|
+
return '<div class="kb-item" data-file="knowledge/' + escHtml(item.category) + '/' + escHtml(item.file) + '" onclick="kbOpenItem(\'' + escHtml(item.category) + '\', \'' + escHtml(item.file) + '\')">' +
|
|
62
|
+
'<div class="kb-item-body">' +
|
|
63
|
+
'<div class="kb-item-title">' + icon + ' ' + escHtml(item.title) + '</div>' +
|
|
64
|
+
'<div class="kb-item-meta">' +
|
|
65
|
+
'<span>' + label + '</span>' +
|
|
66
|
+
(item.agent ? '<span>' + item.agent + '</span>' : '') +
|
|
67
|
+
'<span>' + (item.date || '') + '</span>' +
|
|
68
|
+
'<span>' + Math.round(item.size / 1024) + 'KB</span>' +
|
|
69
|
+
'</div>' +
|
|
70
|
+
(item.preview ? '<div class="kb-item-preview">' + escHtml(item.preview) + '</div>' : '') +
|
|
71
|
+
'</div>' +
|
|
72
|
+
'</div>';
|
|
73
|
+
}).join('');
|
|
74
|
+
restoreNotifBadges();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function kbSetTab(tab) {
|
|
78
|
+
_kbActiveTab = tab;
|
|
79
|
+
renderKnowledgeBase();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function kbSweep() {
|
|
83
|
+
const btn = document.getElementById('kb-sweep-btn');
|
|
84
|
+
const origText = btn.textContent;
|
|
85
|
+
btn.disabled = true;
|
|
86
|
+
btn.textContent = 'sweeping...';
|
|
87
|
+
btn.style.color = 'var(--blue)';
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch('/api/knowledge/sweep', { method: 'POST', signal: AbortSignal.timeout(300000) });
|
|
90
|
+
const data = await res.json();
|
|
91
|
+
if (data.ok) {
|
|
92
|
+
btn.textContent = 'done';
|
|
93
|
+
btn.style.color = 'var(--green)';
|
|
94
|
+
showToast('cmd-toast', 'KB sweep: ' + (data.summary || 'complete'), true);
|
|
95
|
+
refreshKnowledgeBase();
|
|
96
|
+
} else {
|
|
97
|
+
btn.style.color = 'var(--red)';
|
|
98
|
+
btn.textContent = 'failed';
|
|
99
|
+
showToast('cmd-toast', 'Sweep failed: ' + (data.error || 'unknown'), false);
|
|
100
|
+
}
|
|
101
|
+
} catch (e) {
|
|
102
|
+
btn.style.color = 'var(--red)';
|
|
103
|
+
btn.textContent = 'failed';
|
|
104
|
+
showToast('cmd-toast', 'Sweep error: ' + e.message, false);
|
|
105
|
+
}
|
|
106
|
+
setTimeout(() => { btn.textContent = origText; btn.style.color = 'var(--muted)'; btn.disabled = false; }, 3000);
|
|
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
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// render-other.js — Projects, MCP servers, metrics, token usage renderers extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
function renderProjects(projects) {
|
|
4
|
+
const header = document.getElementById('header-projects');
|
|
5
|
+
const list = document.getElementById('projects-list');
|
|
6
|
+
if (!projects.length) {
|
|
7
|
+
header.textContent = 'No projects';
|
|
8
|
+
list.innerHTML = '<span style="color:var(--muted);font-style:italic">No projects linked. Run: node minions.js add <dir></span>';
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
header.textContent = projects.map(p => p.name).join(' + ');
|
|
12
|
+
list.innerHTML = projects.map(p =>
|
|
13
|
+
'<span title="' + escHtml(p.description || p.path || '') + '" style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--blue);font-weight:500;cursor:help">' +
|
|
14
|
+
escHtml(p.name) +
|
|
15
|
+
(p.description ? '<span style="color:var(--muted);font-weight:400;margin-left:6px;font-size:10px">' + escHtml(p.description.slice(0, 60)) + (p.description.length > 60 ? '...' : '') + '</span>' : '') +
|
|
16
|
+
'</span>'
|
|
17
|
+
).join('') +
|
|
18
|
+
'<span onclick="addProject()" style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--muted);font-weight:500;cursor:pointer;border-style:dashed">+ Add</span>';
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function renderMcpServers(servers) {
|
|
23
|
+
const el = document.getElementById('mcp-list');
|
|
24
|
+
const countEl = document.getElementById('mcp-count');
|
|
25
|
+
countEl.textContent = servers.length;
|
|
26
|
+
if (!servers.length) {
|
|
27
|
+
el.innerHTML = '<p class="empty">No MCP servers found. Add them to <code>~/.claude.json</code> and they\'ll appear here automatically.</p>';
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
el.innerHTML = '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px">' +
|
|
31
|
+
servers.map(s =>
|
|
32
|
+
'<div style="font-size:11px;padding:5px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text)" title="' + escHtml(s.args || s.command) + '">' +
|
|
33
|
+
escHtml(s.name) +
|
|
34
|
+
'</div>'
|
|
35
|
+
).join('') +
|
|
36
|
+
'</div>' +
|
|
37
|
+
'<p style="font-size:10px;color:var(--muted);margin:0">Synced from <code style="color:var(--blue)">~/.claude.json</code> — add MCP servers there to make them available to all agents.</p>';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderMetrics(metrics) {
|
|
41
|
+
const el = document.getElementById('metrics-content');
|
|
42
|
+
const agents = Object.entries(metrics).filter(([k]) => !k.startsWith('_'));
|
|
43
|
+
if (agents.length === 0) {
|
|
44
|
+
el.innerHTML = '<p class="empty">No metrics yet. Metrics appear after agents complete tasks.</p>';
|
|
45
|
+
renderTokenUsage(metrics);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let html = '<table class="pr-table"><thead><tr><th>Agent</th><th>Done</th><th>Errors</th><th>PRs</th><th>Approved</th><th>Rejected</th><th>Rate</th><th>Reviews</th></tr></thead><tbody>';
|
|
49
|
+
for (const [id, m] of agents) {
|
|
50
|
+
const rate = m.prsCreated > 0 ? Math.round((m.prsApproved / m.prsCreated) * 100) + '%' : '-';
|
|
51
|
+
const rateColor = m.prsCreated > 0 ? (m.prsApproved / m.prsCreated >= 0.7 ? 'var(--green)' : 'var(--red)') : 'var(--muted)';
|
|
52
|
+
html += '<tr>' +
|
|
53
|
+
'<td style="font-weight:600">' + escHtml(id) + '</td>' +
|
|
54
|
+
'<td style="color:var(--green)">' + (m.tasksCompleted || 0) + '</td>' +
|
|
55
|
+
'<td style="color:' + (m.tasksErrored > 0 ? 'var(--red)' : 'var(--muted)') + '">' + (m.tasksErrored || 0) + '</td>' +
|
|
56
|
+
'<td>' + (m.prsCreated || 0) + '</td>' +
|
|
57
|
+
'<td style="color:var(--green)">' + (m.prsApproved || 0) + '</td>' +
|
|
58
|
+
'<td style="color:' + (m.prsRejected > 0 ? 'var(--red)' : 'var(--muted)') + '">' + (m.prsRejected || 0) + '</td>' +
|
|
59
|
+
'<td style="color:' + rateColor + ';font-weight:600">' + rate + '</td>' +
|
|
60
|
+
'<td>' + (m.reviewsDone || 0) + '</td>' +
|
|
61
|
+
'</tr>';
|
|
62
|
+
}
|
|
63
|
+
html += '</tbody></table>';
|
|
64
|
+
el.innerHTML = html;
|
|
65
|
+
renderTokenUsage(metrics);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renderTokenUsage(metrics) {
|
|
69
|
+
const el = document.getElementById('token-usage-content');
|
|
70
|
+
const agents = Object.entries(metrics).filter(([k]) => !k.startsWith('_'));
|
|
71
|
+
const daily = metrics._daily || {};
|
|
72
|
+
const engine = metrics._engine || {};
|
|
73
|
+
|
|
74
|
+
// Aggregate agent totals
|
|
75
|
+
let agentCost = 0, agentInput = 0, agentOutput = 0, agentCache = 0;
|
|
76
|
+
for (const [, m] of agents) {
|
|
77
|
+
agentCost += m.totalCostUsd || 0;
|
|
78
|
+
agentInput += m.totalInputTokens || 0;
|
|
79
|
+
agentOutput += m.totalOutputTokens || 0;
|
|
80
|
+
agentCache += m.totalCacheRead || 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Aggregate engine totals
|
|
84
|
+
let engineCost = 0, engineInput = 0, engineOutput = 0, engineCache = 0, engineCalls = 0;
|
|
85
|
+
for (const [, e] of Object.entries(engine)) {
|
|
86
|
+
engineCost += e.costUsd || 0;
|
|
87
|
+
engineInput += e.inputTokens || 0;
|
|
88
|
+
engineOutput += e.outputTokens || 0;
|
|
89
|
+
engineCache += e.cacheRead || 0;
|
|
90
|
+
engineCalls += e.calls || 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const totalCost = agentCost + engineCost;
|
|
94
|
+
const totalInput = agentInput + engineInput;
|
|
95
|
+
const totalOutput = agentOutput + engineOutput;
|
|
96
|
+
const totalCache = agentCache + engineCache;
|
|
97
|
+
|
|
98
|
+
if (totalCost === 0 && Object.keys(daily).length === 0) {
|
|
99
|
+
el.innerHTML = '<p class="empty">No usage data yet. Token tracking starts on next agent completion.</p>';
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const fmtTokens = (n) => n >= 1000000 ? (n / 1000000).toFixed(1) + 'M' : n >= 1000 ? (n / 1000).toFixed(0) + 'K' : String(n);
|
|
104
|
+
const fmtCost = (n) => '$' + n.toFixed(2);
|
|
105
|
+
|
|
106
|
+
// Summary tiles
|
|
107
|
+
let html = '<div class="token-tiles">';
|
|
108
|
+
html += '<div class="token-tile"><div class="token-tile-label">Total Cost</div><div class="token-tile-value">' + fmtCost(totalCost) + '</div></div>';
|
|
109
|
+
html += '<div class="token-tile"><div class="token-tile-label">Input Tokens</div><div class="token-tile-value">' + fmtTokens(totalInput) + '</div></div>';
|
|
110
|
+
html += '<div class="token-tile"><div class="token-tile-label">Output Tokens</div><div class="token-tile-value">' + fmtTokens(totalOutput) + '</div></div>';
|
|
111
|
+
html += '<div class="token-tile"><div class="token-tile-label">Cache Reads</div><div class="token-tile-value">' + fmtTokens(totalCache) + '</div></div>';
|
|
112
|
+
|
|
113
|
+
// Today's cost
|
|
114
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
115
|
+
const todayData = daily[today];
|
|
116
|
+
if (todayData) {
|
|
117
|
+
html += '<div class="token-tile"><div class="token-tile-label">Today</div><div class="token-tile-value">' + fmtCost(todayData.costUsd) + '</div><div class="token-tile-sub">' + (todayData.tasks || 0) + ' tasks</div></div>';
|
|
118
|
+
}
|
|
119
|
+
html += '</div>';
|
|
120
|
+
|
|
121
|
+
// Daily bar chart (last 14 days)
|
|
122
|
+
const days = Object.keys(daily).sort().slice(-14);
|
|
123
|
+
if (days.length > 1) {
|
|
124
|
+
const maxCost = Math.max(...days.map(d => daily[d].costUsd || 0), 0.01);
|
|
125
|
+
html += '<div style="font-size:10px;color:var(--muted);margin:8px 0 4px">Daily Cost (last ' + days.length + ' days)</div>';
|
|
126
|
+
html += '<div class="token-chart">';
|
|
127
|
+
for (const day of days) {
|
|
128
|
+
const d = daily[day];
|
|
129
|
+
const pct = Math.max(((d.costUsd || 0) / maxCost) * 100, 2);
|
|
130
|
+
html += '<div class="token-bar" style="height:' + pct + '%"><div class="token-bar-tip">' + day.slice(5) + ': ' + fmtCost(d.costUsd) + ' / ' + (d.tasks || 0) + ' tasks</div></div>';
|
|
131
|
+
}
|
|
132
|
+
html += '</div>';
|
|
133
|
+
html += '<div class="token-chart-labels">';
|
|
134
|
+
for (const day of days) {
|
|
135
|
+
html += '<span>' + day.slice(8) + '</span>';
|
|
136
|
+
}
|
|
137
|
+
html += '</div>';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Per-agent token table
|
|
141
|
+
const agentsWithUsage = agents.filter(([, m]) => (m.totalCostUsd || 0) > 0);
|
|
142
|
+
if (agentsWithUsage.length > 0) {
|
|
143
|
+
html += '<div style="font-size:10px;color:var(--muted);margin:12px 0 4px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">Agent Usage</div>';
|
|
144
|
+
html += '<table class="token-agent-table"><thead><tr><th>Agent</th><th>Cost</th><th>Input</th><th>Output</th><th>Cache</th><th>$/task</th></tr></thead><tbody>';
|
|
145
|
+
for (const [id, m] of agentsWithUsage.sort((a, b) => (b[1].totalCostUsd || 0) - (a[1].totalCostUsd || 0))) {
|
|
146
|
+
const tasks = (m.tasksCompleted || 0) + (m.tasksErrored || 0);
|
|
147
|
+
const perTask = tasks > 0 ? fmtCost((m.totalCostUsd || 0) / tasks) : '-';
|
|
148
|
+
html += '<tr>' +
|
|
149
|
+
'<td style="font-weight:600">' + escHtml(id) + '</td>' +
|
|
150
|
+
'<td>' + fmtCost(m.totalCostUsd || 0) + '</td>' +
|
|
151
|
+
'<td>' + fmtTokens(m.totalInputTokens || 0) + '</td>' +
|
|
152
|
+
'<td>' + fmtTokens(m.totalOutputTokens || 0) + '</td>' +
|
|
153
|
+
'<td>' + fmtTokens(m.totalCacheRead || 0) + '</td>' +
|
|
154
|
+
'<td style="color:var(--muted)">' + perTask + '</td>' +
|
|
155
|
+
'</tr>';
|
|
156
|
+
}
|
|
157
|
+
html += '</tbody></table>';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Engine (Haiku) usage table
|
|
161
|
+
const engineEntries = Object.entries(engine).filter(([, e]) => (e.costUsd || 0) > 0 || (e.calls || 0) > 0);
|
|
162
|
+
if (engineEntries.length > 0) {
|
|
163
|
+
const labels = { 'consolidation': 'Consolidation', 'command-center': 'Command Center', 'doc-chat': 'Doc Chat' };
|
|
164
|
+
html += '<div style="font-size:10px;color:var(--muted);margin:12px 0 4px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">Engine Usage (Haiku)</div>';
|
|
165
|
+
html += '<table class="token-agent-table"><thead><tr><th>Operation</th><th>Cost</th><th>Calls</th><th>Input</th><th>Output</th><th>$/call</th></tr></thead><tbody>';
|
|
166
|
+
for (const [cat, e] of engineEntries.sort((a, b) => (b[1].costUsd || 0) - (a[1].costUsd || 0))) {
|
|
167
|
+
const perCall = (e.calls || 0) > 0 ? fmtCost((e.costUsd || 0) / e.calls) : '-';
|
|
168
|
+
html += '<tr>' +
|
|
169
|
+
'<td style="font-weight:600">' + escHtml(labels[cat] || cat) + '</td>' +
|
|
170
|
+
'<td>' + fmtCost(e.costUsd || 0) + '</td>' +
|
|
171
|
+
'<td>' + (e.calls || 0) + '</td>' +
|
|
172
|
+
'<td>' + fmtTokens(e.inputTokens || 0) + '</td>' +
|
|
173
|
+
'<td>' + fmtTokens(e.outputTokens || 0) + '</td>' +
|
|
174
|
+
'<td style="color:var(--muted)">' + perCall + '</td>' +
|
|
175
|
+
'</tr>';
|
|
176
|
+
}
|
|
177
|
+
html += '</tbody></table>';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
el.innerHTML = html;
|
|
181
|
+
}
|