@yemi33/minions 0.1.2048 → 0.1.2049
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/command-center.js +72 -0
- package/dashboard/js/command-history.js +2 -1
- package/dashboard/js/command-input.js +7 -3
- package/dashboard/js/command-parser.js +1 -0
- package/dashboard/js/detail-panel.js +7 -0
- package/dashboard/js/fre.js +1 -0
- package/dashboard/js/live-stream.js +2 -0
- package/dashboard/js/modal-qa.js +9 -0
- package/dashboard/js/modal.js +1 -0
- package/dashboard/js/qa.js +6 -0
- package/dashboard/js/refresh.js +3 -1
- package/dashboard/js/render-agents.js +30 -3
- package/dashboard/js/render-dispatch.js +12 -0
- package/dashboard/js/render-inbox.js +6 -0
- package/dashboard/js/render-kb.js +4 -0
- package/dashboard/js/render-managed.js +1 -0
- package/dashboard/js/render-meetings.js +6 -0
- package/dashboard/js/render-other.js +11 -0
- package/dashboard/js/render-pinned.js +2 -0
- package/dashboard/js/render-pipelines.js +5 -0
- package/dashboard/js/render-plans.js +9 -0
- package/dashboard/js/render-prd.js +8 -0
- package/dashboard/js/render-prs.js +4 -0
- package/dashboard/js/render-schedules.js +5 -0
- package/dashboard/js/render-skills.js +2 -0
- package/dashboard/js/render-watches.js +10 -0
- package/dashboard/js/render-work-items.js +9 -0
- package/dashboard/js/settings.js +10 -0
- package/dashboard/slim.html +8 -8
- package/docs/deprecated.json +7 -3
- package/engine/lifecycle.js +0 -1
- package/engine/queries.js +37 -5
- package/package.json +6 -3
- package/playbooks/fix.md +15 -5
- package/playbooks/shared-rules.md +29 -0
|
@@ -18,6 +18,7 @@ function renderDetailTabs(detail) {
|
|
|
18
18
|
{ id: 'history', label: 'History' },
|
|
19
19
|
{ id: 'output', label: 'Output Log' },
|
|
20
20
|
];
|
|
21
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: composed from compile-time constants (no user data flows in)
|
|
21
22
|
document.getElementById('detail-tabs').innerHTML = tabs.map(t =>
|
|
22
23
|
'<div class="detail-tab ' + (t.id === currentTab ? 'active' : '') + '" onclick="switchTab(\'' + t.id + '\')">' + t.label + '</div>'
|
|
23
24
|
).join('');
|
|
@@ -89,9 +90,11 @@ function renderDetailContent(detail, tab) {
|
|
|
89
90
|
html += '<h4>Latest Output</h4><div class="section">' + renderMd(detail.outputLog) + '</div>';
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml()/renderMd() (fields: status task, dispatch task/result/reason, inbox name/content, output log, result summary)
|
|
92
94
|
el.innerHTML = html;
|
|
93
95
|
} else if (tab === 'live') {
|
|
94
96
|
var startedAt = detail.statusData?.started_at;
|
|
97
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: composed from internal server timestamp and static live-chat controls (no user data flows in)
|
|
95
98
|
el.innerHTML =
|
|
96
99
|
'<div id="live-chat" style="display:flex;flex-direction:column;height:60vh">' +
|
|
97
100
|
'<div id="live-messages" style="flex:1;overflow-y:auto;padding:8px;font-size:11px;line-height:1.6;display:flex;flex-direction:column"></div>' +
|
|
@@ -110,6 +113,7 @@ function renderDetailContent(detail, tab) {
|
|
|
110
113
|
startLiveStream(currentAgentId);
|
|
111
114
|
} else if (tab === 'charter') {
|
|
112
115
|
const charterContent = detail.charter || '';
|
|
116
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes charter display content and escHtml() escapes textarea content before assembling HTML (see dashboard/js/utils.js)
|
|
113
117
|
el.innerHTML =
|
|
114
118
|
'<div style="display:flex;gap:6px;margin-bottom:8px">' +
|
|
115
119
|
'<button class="pr-pager-btn" id="charter-edit-btn" style="font-size:10px;padding:2px 10px" onclick="_toggleCharterEdit()">Edit</button>' +
|
|
@@ -139,8 +143,10 @@ function renderDetailContent(detail, tab) {
|
|
|
139
143
|
}
|
|
140
144
|
// Raw history.md
|
|
141
145
|
html += '<h4>Task History</h4><div class="section">' + renderMd(detail.history || 'No history yet.') + '</div>';
|
|
146
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml()/renderMd() (fields: dispatch task, type, result, reason, history)
|
|
142
147
|
el.innerHTML = html;
|
|
143
148
|
} else if (tab === 'output') {
|
|
149
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderAgentOutput() escapes all user-controlled fields before assembling HTML (see dashboard/js/render-utils.js)
|
|
144
150
|
el.innerHTML = '<div class="section">' + (detail.outputLog ? renderAgentOutput(detail.outputLog) : 'No output log. The coordinator will save agent output here when tasks complete.') + '</div>';
|
|
145
151
|
}
|
|
146
152
|
}
|
|
@@ -175,6 +181,7 @@ async function _saveCharter() {
|
|
|
175
181
|
body: JSON.stringify({ agent: currentAgentId, content })
|
|
176
182
|
});
|
|
177
183
|
if (res.ok) {
|
|
184
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
|
|
178
185
|
document.getElementById('charter-view').innerHTML = renderMd(content);
|
|
179
186
|
_charterRawCache = content;
|
|
180
187
|
_cancelCharterEdit();
|
package/dashboard/js/fre.js
CHANGED
|
@@ -158,6 +158,7 @@ function renderFre(statusOrProjects) {
|
|
|
158
158
|
return ({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' })[c];
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; runtime label is escaped into safeRuntime before interpolation (field: runtimeCli)
|
|
161
162
|
mount.innerHTML =
|
|
162
163
|
'<div id="fre-card" style="' + cardStyle + '">' +
|
|
163
164
|
'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">' +
|
|
@@ -28,6 +28,7 @@ function renderLiveChatMessage(raw) {
|
|
|
28
28
|
const el = document.getElementById('live-messages');
|
|
29
29
|
if (!el) return;
|
|
30
30
|
const html = renderAgentOutput(raw);
|
|
31
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: renderAgentOutput() escapes all user-controlled fields before assembling HTML (see dashboard/js/render-utils.js)
|
|
31
32
|
if (html) el.insertAdjacentHTML('beforeend', html);
|
|
32
33
|
|
|
33
34
|
// Auto-scroll
|
|
@@ -106,6 +107,7 @@ async function sendSteering() {
|
|
|
106
107
|
// Immediate feedback — show the message right away
|
|
107
108
|
const el = document.getElementById('live-messages');
|
|
108
109
|
if (el) {
|
|
110
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: steering message)
|
|
109
111
|
el.insertAdjacentHTML('beforeend', '<div style="align-self:flex-end;background:var(--blue);color:#fff;padding:6px 12px;border-radius:12px 12px 2px 12px;max-width:80%;margin:4px 0;font-size:12px">' + escHtml(message) +
|
|
110
112
|
'<div id="steer-pending" style="font-size:9px;opacity:0.7;margin-top:2px">\u2197 Sending...</div></div>');
|
|
111
113
|
el.scrollTop = el.scrollHeight;
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -63,7 +63,9 @@ function _qaMaybeScrollThreadToBottom(thread, shouldFollow) {
|
|
|
63
63
|
function _qaInsertBeforeQueued(container, html) {
|
|
64
64
|
if (!container) return;
|
|
65
65
|
const firstQueued = container.querySelector && container.querySelector('.qa-queued-item');
|
|
66
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: html is produced by modal QA builders that wrap user data in escHtml()/renderMd() before insertion
|
|
66
67
|
if (firstQueued) firstQueued.insertAdjacentHTML('beforebegin', html);
|
|
68
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: html is produced by modal QA builders that wrap user data in escHtml()/renderMd() before insertion
|
|
67
69
|
else container.insertAdjacentHTML('beforeend', html);
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -268,8 +270,10 @@ function _qaLoadSessionState(key) {
|
|
|
268
270
|
const thread = _qaThreadEl();
|
|
269
271
|
if (thread) {
|
|
270
272
|
const tmp = document.createElement('div');
|
|
273
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: prior.threadHtml is a sanitized modal QA thread buffer produced by escHtml()/renderMd()-wrapped builders
|
|
271
274
|
tmp.innerHTML = prior?.threadHtml || '';
|
|
272
275
|
if (!_qaProcessing) tmp.querySelectorAll('.modal-qa-loading').forEach(el => el.remove());
|
|
276
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: tmp.innerHTML is copied from sanitized modal QA thread markup after removing loading placeholders
|
|
273
277
|
thread.innerHTML = tmp.innerHTML;
|
|
274
278
|
}
|
|
275
279
|
}
|
|
@@ -404,6 +408,7 @@ function _qaBuildLiveProgressHtml(loadingId, label, elapsedSeconds, streamedText
|
|
|
404
408
|
|
|
405
409
|
function _qaMutateThreadHtml(key, mutate) {
|
|
406
410
|
const tmp = document.createElement('div');
|
|
411
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: threadHtml is a sanitized modal QA thread buffer produced by escHtml()/renderMd()-wrapped builders
|
|
407
412
|
tmp.innerHTML = _qaIsActiveSession(key) ? _qaThreadHtml() : ((_qaSessions.get(key) || {}).threadHtml || '');
|
|
408
413
|
mutate(tmp);
|
|
409
414
|
const html = tmp.innerHTML;
|
|
@@ -412,6 +417,7 @@ function _qaMutateThreadHtml(key, mutate) {
|
|
|
412
417
|
const thread = _qaThreadEl();
|
|
413
418
|
if (thread) {
|
|
414
419
|
const shouldFollow = _qaShouldFollowThread(thread);
|
|
420
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: html is a sanitized modal QA thread buffer produced by escHtml()/renderMd()-wrapped builders
|
|
415
421
|
thread.innerHTML = html;
|
|
416
422
|
_qaMaybeScrollThreadToBottom(thread, shouldFollow);
|
|
417
423
|
}
|
|
@@ -570,6 +576,7 @@ function modalSend() {
|
|
|
570
576
|
return;
|
|
571
577
|
}
|
|
572
578
|
_qaQueue.push({ message, selection });
|
|
579
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: queued message)
|
|
573
580
|
thread.insertAdjacentHTML('beforeend', _qaBuildQueuedHtml(message));
|
|
574
581
|
_qaScrollThreadToBottom(thread);
|
|
575
582
|
_showThreadWrap();
|
|
@@ -654,6 +661,7 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
654
661
|
const updatedThreadHtml = _qaMutateThreadHtml(sessionKey, tmp => {
|
|
655
662
|
const loadingEl = tmp.querySelector('#' + loadingId);
|
|
656
663
|
if (!loadingEl) return;
|
|
664
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: _qaBuildLiveProgressHtml() uses renderMd()/formatToolSummary() to escape streamed text and tool fields before assembling HTML
|
|
657
665
|
loadingEl.innerHTML = _qaBuildLiveProgressHtml(
|
|
658
666
|
loadingId,
|
|
659
667
|
_qaProgressLabel(elapsed),
|
|
@@ -813,6 +821,7 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
813
821
|
if (isJson) {
|
|
814
822
|
body.textContent = display;
|
|
815
823
|
} else {
|
|
824
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
|
|
816
825
|
body.innerHTML = renderMd(display);
|
|
817
826
|
body.style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
818
827
|
body.style.whiteSpace = 'normal';
|
package/dashboard/js/modal.js
CHANGED
|
@@ -122,6 +122,7 @@ function renderArchiveButtons(archives) {
|
|
|
122
122
|
archivedPrds = archives;
|
|
123
123
|
const el = document.getElementById('archive-btns');
|
|
124
124
|
if (!archives.length) { el.innerHTML = ''; return; }
|
|
125
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: archive version)
|
|
125
126
|
el.innerHTML = archives.map((a, i) =>
|
|
126
127
|
'<button class="archive-btn" onclick="openArchive(' + i + ')">Archived: ' + escHtml(a.version) + ' (' + a.total + ' items)</button>'
|
|
127
128
|
).join(' ');
|
package/dashboard/js/qa.js
CHANGED
|
@@ -56,6 +56,7 @@ async function loadQaTargets() {
|
|
|
56
56
|
} catch (e) { err = e; }
|
|
57
57
|
|
|
58
58
|
if (err) {
|
|
59
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: err.message)
|
|
59
60
|
const frag = document.createRange().createContextualFragment(
|
|
60
61
|
'<p class="empty" style="color:var(--red)">Failed to load targets: ' + escHtml(err.message || String(err)) + '</p>'
|
|
61
62
|
);
|
|
@@ -134,6 +135,7 @@ async function loadQaTargets() {
|
|
|
134
135
|
// the dynamic-innerHTML regression gate (cf. test/unit.test.js
|
|
135
136
|
// DYNAMIC_INNERHTML_BASELINE — all interpolated fields above are wrapped
|
|
136
137
|
// in escHtml()).
|
|
138
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: target name, project, source, ports)
|
|
137
139
|
const frag = document.createRange().createContextualFragment(rows);
|
|
138
140
|
root.replaceChildren(frag);
|
|
139
141
|
}
|
|
@@ -176,6 +178,7 @@ async function loadQaRunbooks() {
|
|
|
176
178
|
_qaRunbooksCache = items;
|
|
177
179
|
|
|
178
180
|
if (err) {
|
|
181
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: err.message)
|
|
179
182
|
const frag = document.createRange().createContextualFragment(
|
|
180
183
|
'<p class="empty" style="color:var(--red)">Failed to load runbooks: ' + escHtml(err.message || String(err)) + '</p>'
|
|
181
184
|
);
|
|
@@ -204,6 +207,7 @@ async function loadQaRunbooks() {
|
|
|
204
207
|
'<button class="qa-btn-primary qa-runbook-run-btn" data-runbook-id="' + escHtml(id) + '" onclick="qaRunRunbook(this.dataset.runbookId)">Run</button>' +
|
|
205
208
|
'</div>';
|
|
206
209
|
}).join('');
|
|
210
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: runbook id, name, target)
|
|
207
211
|
const frag = document.createRange().createContextualFragment(rows);
|
|
208
212
|
root.replaceChildren(frag);
|
|
209
213
|
}
|
|
@@ -337,6 +341,7 @@ async function loadQaRuns() {
|
|
|
337
341
|
} catch (e) { err = e; }
|
|
338
342
|
|
|
339
343
|
if (err) {
|
|
344
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: err.message)
|
|
340
345
|
const frag = document.createRange().createContextualFragment(
|
|
341
346
|
'<p class="empty" style="color:var(--red)">Failed to load runs: ' + escHtml(err.message || String(err)) + '</p>'
|
|
342
347
|
);
|
|
@@ -351,6 +356,7 @@ async function loadQaRuns() {
|
|
|
351
356
|
return;
|
|
352
357
|
}
|
|
353
358
|
const rows = items.map(_qaRenderRunRow).join('');
|
|
359
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: run id, status, runbook, target, timestamp, workItemId, agentId, artifact file names)
|
|
354
360
|
const frag = document.createRange().createContextualFragment(rows);
|
|
355
361
|
root.replaceChildren(frag);
|
|
356
362
|
}
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -64,7 +64,7 @@ function _detectPageChanges(data) {
|
|
|
64
64
|
// for the same input. Cross-restart safety lives in the dashboardBuildId
|
|
65
65
|
// reload path below — RENDER_VERSIONS handles the within-process case.
|
|
66
66
|
const RENDER_VERSIONS = {
|
|
67
|
-
agents:
|
|
67
|
+
agents: 2,
|
|
68
68
|
prdProgress: 1,
|
|
69
69
|
prdPrs: 1,
|
|
70
70
|
inbox: 2,
|
|
@@ -166,6 +166,7 @@ function _processStatusUpdate(data) {
|
|
|
166
166
|
const engineState = (data.engine && data.engine.state) ? data.engine.state : 'stopped';
|
|
167
167
|
document.getElementById('setup-banner').style.display = (!data.initialized && engineState !== 'stopped') ? 'block' : 'none';
|
|
168
168
|
const autoEl = document.getElementById('auto-approve-badge');
|
|
169
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: composed from compile-time constants (no user data flows in)
|
|
169
170
|
if (autoEl) autoEl.innerHTML = data.autoMode?.approvePlans
|
|
170
171
|
? '<span style="font-size:9px;font-weight:600;padding:1px 6px;border-radius:3px;background:rgba(63,185,80,0.15);color:var(--green);border:1px solid rgba(63,185,80,0.3)">AUTO-APPROVE</span>'
|
|
171
172
|
: '';
|
|
@@ -242,6 +243,7 @@ function _processStatusUpdate(data) {
|
|
|
242
243
|
var wt = data.engine.worktreeCount != null ? data.engine.worktreeCount : '-';
|
|
243
244
|
var tick = data.engine.tick || '-';
|
|
244
245
|
var pid = data.engine.pid || '-';
|
|
246
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: composed from internal engine metrics (pid, tick, worktreeCount; no user data flows in)
|
|
245
247
|
qs.innerHTML = '<span>PID: <b>' + pid + '</b></span><span>Tick: <b>' + tick + '</b></span><span>Worktrees: <b>' + wt + '</b></span>';
|
|
246
248
|
}
|
|
247
249
|
}
|
|
@@ -35,13 +35,23 @@ function _runtimeTagHtml(runtime) {
|
|
|
35
35
|
return '<span class="agent-runtime-tag" title="Runtime: ' + escapeHtml(fallback) + '" style="font-size:9px;font-weight:600;letter-spacing:0.4px;text-transform:uppercase;padding:1px 5px;margin-left:6px;border:1px solid var(--muted);border-radius:3px;color:var(--muted);background:transparent">' + escapeHtml(fallback) + '</span>';
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// W-mpmwxk4y00053271 — compact text chip showing the resolved model next to
|
|
39
|
+
// the runtime tag. Returns '' when the model is unknown so the row degrades
|
|
40
|
+
// gracefully instead of rendering "null" / "undefined". Caleb feedback:
|
|
41
|
+
// "The model is really critical piece of information to me as a user."
|
|
42
|
+
function _modelChipHtml(model) {
|
|
43
|
+
if (!model || typeof model !== 'string') return '';
|
|
44
|
+
return '<span class="agent-model-tag" title="Model: ' + escapeHtml(model) + '" style="font-size:9px;font-weight:600;letter-spacing:0.4px;padding:1px 5px;margin-left:4px;border:1px solid var(--muted);border-radius:3px;color:var(--muted);background:transparent">' + escapeHtml(model) + '</span>';
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
function renderAgents(agents) {
|
|
39
48
|
agentData = agents;
|
|
40
49
|
const grid = document.getElementById('agents-grid');
|
|
50
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml()/renderMd() (fields: agent id, emoji, name, status, role, lastAction, warning, resultSummary, blocking tool)
|
|
41
51
|
grid.innerHTML = agents.map(a => `
|
|
42
52
|
<div class="agent-card ${statusColor(a.status)}" data-agent-id="${escapeHtml(a.id)}" onclick="if(shouldIgnoreSelectionClick(event))return;openAgentDetail(this.dataset.agentId)">
|
|
43
53
|
<div class="agent-card-header">
|
|
44
|
-
<span class="agent-name"><span class="agent-emoji">${escapeHtml(a.emoji)}</span>${escapeHtml(a.name)}${_runtimeTagHtml(a.runtime)}</span>
|
|
54
|
+
<span class="agent-name"><span class="agent-emoji">${escapeHtml(a.emoji)}</span>${escapeHtml(a.name)}${_runtimeTagHtml(a.runtime)}${_modelChipHtml(a.model)}</span>
|
|
45
55
|
<span class="status-badge ${escapeHtml(a.status)}">${escapeHtml(a.status)}</span>
|
|
46
56
|
</div>
|
|
47
57
|
<div class="agent-role">${escapeHtml(a.role)}</div>
|
|
@@ -83,19 +93,35 @@ async function openAgentDetail(id) {
|
|
|
83
93
|
runtimeSpan.style.cssText = 'display:inline-block;margin-left:10px';
|
|
84
94
|
if (runtimeMeta && runtimeMeta.svg) {
|
|
85
95
|
runtimeSpan.style.color = runtimeMeta.color;
|
|
96
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: composed from compile-time constants (no user data flows in)
|
|
86
97
|
runtimeSpan.innerHTML = runtimeMeta.svg.replace('width="13"', 'width="18"').replace('height="13"', 'height="18"');
|
|
87
98
|
runtimeSpan.setAttribute('aria-label', runtimeMeta.label + ' runtime');
|
|
88
99
|
} else {
|
|
89
100
|
runtimeSpan.style.cssText += ';font-size:10px;font-weight:600;letter-spacing:0.4px;text-transform:uppercase;padding:2px 6px;border:1px solid var(--muted);border-radius:3px;color:var(--muted)';
|
|
90
101
|
runtimeSpan.textContent = agent.runtime || 'unknown';
|
|
91
102
|
}
|
|
92
|
-
|
|
103
|
+
// W-mpmwxk4y00053271 — mirror the model chip the card shows so the detail
|
|
104
|
+
// header stays in sync. textContent path keeps the model string from being
|
|
105
|
+
// interpreted as HTML.
|
|
106
|
+
const modelSpan = (agent.model && typeof agent.model === 'string')
|
|
107
|
+
? (() => {
|
|
108
|
+
const s = document.createElement('span');
|
|
109
|
+
s.title = 'Model: ' + agent.model;
|
|
110
|
+
s.style.cssText = 'display:inline-block;margin-left:6px;font-size:10px;font-weight:600;letter-spacing:0.4px;padding:2px 6px;border:1px solid var(--muted);border-radius:3px;color:var(--muted)';
|
|
111
|
+
s.textContent = agent.model;
|
|
112
|
+
return s;
|
|
113
|
+
})()
|
|
114
|
+
: null;
|
|
115
|
+
const children = [
|
|
93
116
|
emojiSpan,
|
|
94
117
|
document.createTextNode(' ' + (agent.name || '') + ' \u2014 ' + (agent.role || '')),
|
|
95
118
|
runtimeSpan,
|
|
96
|
-
|
|
119
|
+
];
|
|
120
|
+
if (modelSpan) children.push(modelSpan);
|
|
121
|
+
nameEl.replaceChildren(...children);
|
|
97
122
|
|
|
98
123
|
const badgeClass = agent.status;
|
|
124
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; status is an internal bounded enum and all user data is wrapped in escapeHtml()/renderMd() (fields: lastAction, blocking tool, resultSummary)
|
|
99
125
|
document.getElementById('detail-status-line').innerHTML =
|
|
100
126
|
'<span class="status-badge ' + badgeClass + '">' + agent.status.toUpperCase() + '</span> ' +
|
|
101
127
|
'<span style="color:var(--muted)">' + escapeHtml(agent.lastAction) + '</span>' +
|
|
@@ -112,6 +138,7 @@ async function openAgentDetail(id) {
|
|
|
112
138
|
renderDetailTabs(detail);
|
|
113
139
|
renderDetailContent(detail, currentTab);
|
|
114
140
|
} catch(e) {
|
|
141
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: error message, agent id)
|
|
115
142
|
document.getElementById('detail-content').innerHTML =
|
|
116
143
|
'<div style="padding:24px;text-align:center">' +
|
|
117
144
|
'<div style="color:var(--red);margin-bottom:12px">Error loading agent detail: ' + escapeHtml(e.message) + '</div>' +
|
|
@@ -101,6 +101,7 @@ function _wireEngineRestartClick(button) {
|
|
|
101
101
|
|
|
102
102
|
function _renderEngineRestartSuccessBanner(el) {
|
|
103
103
|
const pid = _engineRestartState?.pid || '?';
|
|
104
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: composed from internal restart PID and fixed status markup (no user data flows in)
|
|
104
105
|
el.innerHTML =
|
|
105
106
|
'<span class="engine-alert-msg" style="color:var(--green)">✓ Engine restarted (PID ' + pid + ') — waiting for first heartbeat...</span>';
|
|
106
107
|
el.style.display = 'flex';
|
|
@@ -108,6 +109,7 @@ function _renderEngineRestartSuccessBanner(el) {
|
|
|
108
109
|
|
|
109
110
|
function _renderEngineRestartRetryBanner(el) {
|
|
110
111
|
const attempts = _engineRestartState?.retryCount || 0;
|
|
112
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: composed from internal restart retry count and fixed action markup (no user data flows in)
|
|
111
113
|
el.innerHTML =
|
|
112
114
|
'<span class="engine-alert-msg">⚠️ Engine restart didn\'t take — heartbeat still stale (attempt ' + attempts + ' of ' + _ENGINE_RESTART_MAX_RETRIES + ').</span>' +
|
|
113
115
|
'<span class="engine-alert-action" id="engine-alert-restart">Retry restart</span>';
|
|
@@ -117,6 +119,7 @@ function _renderEngineRestartRetryBanner(el) {
|
|
|
117
119
|
|
|
118
120
|
function _renderEngineStaleBanner(el, staleMs) {
|
|
119
121
|
const mins = Math.max(1, Math.round(staleMs / 60000));
|
|
122
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: composed from internal heartbeat age and fixed action markup (no user data flows in)
|
|
120
123
|
el.innerHTML =
|
|
121
124
|
'<span class="engine-alert-msg">⚠️ Engine heartbeat is stale (' + mins + 'm old). Dispatch may be stuck.</span>' +
|
|
122
125
|
'<span class="engine-alert-action" id="engine-alert-restart">Restart engine</span>';
|
|
@@ -162,6 +165,7 @@ function renderAdoThrottleAlert(adoThrottle) {
|
|
|
162
165
|
return;
|
|
163
166
|
}
|
|
164
167
|
const resumeTime = formatLocalTime(adoThrottle.retryAfter);
|
|
168
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: composed from internal ADO throttle timestamp and counters (no user data flows in)
|
|
165
169
|
el.innerHTML =
|
|
166
170
|
'<span class="engine-alert-msg">⚠️ ADO rate-limited — resume ~' + resumeTime + ' (' + adoThrottle.consecutiveHits + ' consecutive hit' + (adoThrottle.consecutiveHits !== 1 ? 's' : '') + ')</span>';
|
|
167
171
|
el.style.display = 'flex';
|
|
@@ -176,6 +180,7 @@ function renderGhThrottleAlert(ghThrottle) {
|
|
|
176
180
|
return;
|
|
177
181
|
}
|
|
178
182
|
const resumeTime = formatLocalTime(ghThrottle.retryAfter);
|
|
183
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: composed from internal GitHub throttle timestamp and counters (no user data flows in)
|
|
179
184
|
el.innerHTML =
|
|
180
185
|
'<span class="engine-alert-msg">⚠️ GitHub rate-limited — resume ~' + resumeTime + ' (' + ghThrottle.consecutiveHits + ' consecutive hit' + (ghThrottle.consecutiveHits !== 1 ? 's' : '') + ')</span>';
|
|
181
186
|
el.style.display = 'flex';
|
|
@@ -186,6 +191,7 @@ function renderDispatch(dispatch) {
|
|
|
186
191
|
|
|
187
192
|
// Stats
|
|
188
193
|
const stats = document.getElementById('dispatch-stats');
|
|
194
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: composed from internal dispatch counts (no user data flows in)
|
|
189
195
|
stats.innerHTML =
|
|
190
196
|
'<div class="dispatch-stat"><div class="dispatch-stat-num yellow">' + (dispatch.active || []).length + '</div><div class="dispatch-stat-label">Active</div></div>' +
|
|
191
197
|
'<div class="dispatch-stat"><div class="dispatch-stat-num blue">' + (dispatch.pending || []).length + '</div><div class="dispatch-stat-label">Pending</div></div>' +
|
|
@@ -205,6 +211,7 @@ function renderDispatch(dispatch) {
|
|
|
205
211
|
// Active
|
|
206
212
|
const activeEl = document.getElementById('dispatch-active');
|
|
207
213
|
if ((dispatch.active || []).length > 0) {
|
|
214
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: dispatch type, agent, task)
|
|
208
215
|
activeEl.innerHTML = '<div style="font-size:11px;color:var(--green);margin-bottom:6px;font-weight:600">ACTIVE</div><div class="dispatch-list">' +
|
|
209
216
|
dispatch.active.map(d => dispatchItemHtml(d,
|
|
210
217
|
'<span class="dispatch-time">' + shortTime(d.started_at) + '</span>'
|
|
@@ -216,6 +223,7 @@ function renderDispatch(dispatch) {
|
|
|
216
223
|
// Pending
|
|
217
224
|
const pendingEl = document.getElementById('dispatch-pending');
|
|
218
225
|
if ((dispatch.pending || []).length > 0) {
|
|
226
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: dispatch type, agent, task, skip reason)
|
|
219
227
|
pendingEl.innerHTML = '<div style="font-size:11px;color:var(--yellow);margin:8px 0 6px;font-weight:600">PENDING</div><div class="dispatch-list">' +
|
|
220
228
|
dispatch.pending.map(d => dispatchItemHtml(d,
|
|
221
229
|
d.skipReason ? '<span style="font-size:9px;color:var(--muted);margin-left:6px" title="' + escHtml(d.skipReason) + '">' + escHtml(d.skipReason.replace(/_/g, ' ')) + '</span>' : ''
|
|
@@ -237,6 +245,7 @@ function renderDispatch(dispatch) {
|
|
|
237
245
|
const compStart = _completedPage * COMPLETED_PER_PAGE;
|
|
238
246
|
const pageCompleted = completed.slice(compStart, compStart + COMPLETED_PER_PAGE);
|
|
239
247
|
|
|
248
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: dispatch id, type, agent, task, result, reason)
|
|
240
249
|
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>' +
|
|
241
250
|
pageCompleted.map(d => {
|
|
242
251
|
const isError = d.result === 'error';
|
|
@@ -258,6 +267,7 @@ function renderDispatch(dispatch) {
|
|
|
258
267
|
page: _completedPage, totalPages: totalCompPages,
|
|
259
268
|
onPrev: '_completedPrev()', onNext: '_completedNext()',
|
|
260
269
|
});
|
|
270
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: composed from internal numeric page bounds and fixed renderPager() callback names (no user data flows in)
|
|
261
271
|
if (compPager) completedEl.insertAdjacentHTML('beforeend', compPager);
|
|
262
272
|
} else {
|
|
263
273
|
completedEl.innerHTML = '<p class="empty">No completed dispatches yet.</p>';
|
|
@@ -278,6 +288,7 @@ function renderEngineLog(log) {
|
|
|
278
288
|
const logStart = _logPage * LOG_PER_PAGE;
|
|
279
289
|
const pageLog = reversed.slice(logStart, logStart + LOG_PER_PAGE);
|
|
280
290
|
|
|
291
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: log message)
|
|
281
292
|
el.innerHTML = pageLog.map(e =>
|
|
282
293
|
'<div class="log-entry">' +
|
|
283
294
|
'<span class="log-ts">' + shortTime(e.timestamp) + '</span> ' +
|
|
@@ -290,6 +301,7 @@ function renderEngineLog(log) {
|
|
|
290
301
|
page: _logPage, totalPages: totalLogPages,
|
|
291
302
|
onPrev: '_logPrev()', onNext: '_logNext()',
|
|
292
303
|
});
|
|
304
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: composed from internal numeric page bounds and fixed renderPager() callback names (no user data flows in)
|
|
293
305
|
if (logPager) el.insertAdjacentHTML('beforeend', logPager);
|
|
294
306
|
}
|
|
295
307
|
|
|
@@ -25,6 +25,7 @@ function renderInbox(inbox) {
|
|
|
25
25
|
const inboxStart = _inboxPage * INBOX_PER_PAGE;
|
|
26
26
|
const pageInbox = inbox.slice(inboxStart, inboxStart + INBOX_PER_PAGE);
|
|
27
27
|
|
|
28
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: inbox name, age, content, pin key)
|
|
28
29
|
list.innerHTML = pageInbox.map((item, i) => {
|
|
29
30
|
const idx = inboxStart + i;
|
|
30
31
|
const pk = inboxPinKey(item.name);
|
|
@@ -47,6 +48,7 @@ function renderInbox(inbox) {
|
|
|
47
48
|
page: _inboxPage, totalPages: totalInboxPages,
|
|
48
49
|
onPrev: '_inboxPrev()', onNext: '_inboxNext()',
|
|
49
50
|
});
|
|
51
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: composed from internal numeric page bounds and fixed renderPager() callback names (no user data flows in)
|
|
50
52
|
if (inboxPager) list.insertAdjacentHTML('beforeend', inboxPager);
|
|
51
53
|
restoreNotifBadges();
|
|
52
54
|
}
|
|
@@ -67,6 +69,7 @@ function promoteToKB(name) {
|
|
|
67
69
|
).join('') +
|
|
68
70
|
'</div></div>';
|
|
69
71
|
document.getElementById('modal-title').textContent = 'Add to Knowledge Base';
|
|
72
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: inbox item name)
|
|
70
73
|
document.getElementById('modal-body').innerHTML = picker;
|
|
71
74
|
document.getElementById('modal').classList.add('open');
|
|
72
75
|
}
|
|
@@ -83,6 +86,7 @@ function renderNotes(notes) {
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
if (!content || !content.trim()) { el.innerHTML = '<p class="empty">No team notes yet.</p>'; return; }
|
|
89
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
|
|
86
90
|
el.innerHTML = '<div class="notes-preview" data-file="notes.md" onclick="openNotesModal()" title="Click to expand">' + renderMd(content) + '</div>';
|
|
87
91
|
el.querySelector('.notes-preview')._rawContent = content;
|
|
88
92
|
restoreNotifBadges();
|
|
@@ -93,6 +97,7 @@ function openNotesModal() {
|
|
|
93
97
|
if (!preview) return;
|
|
94
98
|
const content = preview._rawContent || preview.textContent;
|
|
95
99
|
document.getElementById('modal-title').textContent = 'Team Notes';
|
|
100
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
|
|
96
101
|
document.getElementById('modal-body').innerHTML = renderMd(content);
|
|
97
102
|
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
98
103
|
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
@@ -145,6 +150,7 @@ async function modalSaveEdit() {
|
|
|
145
150
|
function modalCancelEdit() {
|
|
146
151
|
const body = document.getElementById('modal-body');
|
|
147
152
|
body.contentEditable = 'false';
|
|
153
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
|
|
148
154
|
body.innerHTML = renderMd(_modalDocContext.content); // revert (render Markdown, not raw text)
|
|
149
155
|
body.style.border = '';
|
|
150
156
|
body.style.padding = '';
|
|
@@ -108,6 +108,7 @@ function renderKnowledgeBase() {
|
|
|
108
108
|
const label = KB_CAT_LABELS[cat] || cat;
|
|
109
109
|
tabsHtml += '<button class="kb-tab ' + (_kbActiveTab === cat ? 'active' : '') + '" onclick="kbSetTab(\'' + cat + '\')">' + label + ' <span class="badge">' + catArr.length + '</span></button>';
|
|
110
110
|
}
|
|
111
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: composed from known KB category constants and item counts (no user data flows in)
|
|
111
112
|
tabsEl.innerHTML = tabsHtml;
|
|
112
113
|
|
|
113
114
|
// Filter items for active tab
|
|
@@ -134,6 +135,7 @@ function renderKnowledgeBase() {
|
|
|
134
135
|
const kbStart = _kbPage * KB_PER_PAGE;
|
|
135
136
|
const pageItems = items.slice(kbStart, kbStart + KB_PER_PAGE);
|
|
136
137
|
|
|
138
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: category, file, title, agent, preview)
|
|
137
139
|
listEl.innerHTML = pageItems.map(item => {
|
|
138
140
|
const icon = KB_CAT_ICONS[item.category] || '\u{1F4C4}';
|
|
139
141
|
const label = KB_CAT_LABELS[item.category] || item.category;
|
|
@@ -159,6 +161,7 @@ function renderKnowledgeBase() {
|
|
|
159
161
|
page: _kbPage, totalPages: totalKbPages,
|
|
160
162
|
onPrev: '_kbPrev()', onNext: '_kbNext()',
|
|
161
163
|
});
|
|
164
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: composed from internal numeric page bounds and fixed renderPager() callback names (no user data flows in)
|
|
162
165
|
if (kbPager) listEl.insertAdjacentHTML('beforeend', kbPager);
|
|
163
166
|
restoreNotifBadges();
|
|
164
167
|
}
|
|
@@ -238,6 +241,7 @@ async function kbOpenItem(category, file) {
|
|
|
238
241
|
const display = content.replace(/^---[\s\S]*?---\n*/m, '');
|
|
239
242
|
document.getElementById('modal-title').textContent = file;
|
|
240
243
|
const modalBody = document.getElementById('modal-body');
|
|
244
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
|
|
241
245
|
modalBody.innerHTML = renderMd(display);
|
|
242
246
|
_modalDocContext = { title: file, content: display, selection: '' };
|
|
243
247
|
_modalFilePath = 'knowledge/' + category + '/' + file; showModalQa();
|
|
@@ -178,6 +178,7 @@ async function renderManagedProcesses() {
|
|
|
178
178
|
// first append so each mount needs its own copy).
|
|
179
179
|
for (const m of liveMounts) {
|
|
180
180
|
if (m.countEl) m.countEl.textContent = countText;
|
|
181
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: fetch error, project, process name, ports, attrs, uptime, ttl)
|
|
181
182
|
const frag = document.createRange().createContextualFragment(html);
|
|
182
183
|
m.root.replaceChildren(frag);
|
|
183
184
|
}
|
|
@@ -30,6 +30,7 @@ function renderMeetings(meetings) {
|
|
|
30
30
|
const visible = _showArchived ? meetings : active;
|
|
31
31
|
if (visible.length === 0) {
|
|
32
32
|
el.innerHTML = '<p class="empty">No active meetings.</p>';
|
|
33
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: composed from internal archived count and fixed toggle markup (no user data flows in)
|
|
33
34
|
if (archived.length) el.insertAdjacentHTML('beforeend', '<div style="text-align:center;margin-top:8px"><button class="pr-pager-btn" style="font-size:10px" onclick="_toggleArchivedMeetings()">Show ' + archived.length + ' archived</button></div>');
|
|
34
35
|
_mtgTotalPages = 1;
|
|
35
36
|
return;
|
|
@@ -41,6 +42,7 @@ function renderMeetings(meetings) {
|
|
|
41
42
|
const start = _mtgPage * MTG_PER_PAGE;
|
|
42
43
|
const pageItems = visible.slice(start, start + MTG_PER_PAGE);
|
|
43
44
|
|
|
45
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: meeting id, title, participants, time, agenda)
|
|
44
46
|
el.innerHTML = pageItems.map(m => {
|
|
45
47
|
const statusColor = statusColors[m.status] || 'var(--muted)';
|
|
46
48
|
const statusLabel = statusLabels[m.status] || m.status;
|
|
@@ -72,6 +74,7 @@ function renderMeetings(meetings) {
|
|
|
72
74
|
}).join('');
|
|
73
75
|
|
|
74
76
|
if (visible.length > MTG_PER_PAGE) {
|
|
77
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: composed from internal numeric page bounds and fixed pager actions (no user data flows in)
|
|
75
78
|
el.insertAdjacentHTML('beforeend', '<div class="pr-pager">' +
|
|
76
79
|
'<span class="pr-page-info">Showing ' + (start + 1) + ' to ' + Math.min(start + MTG_PER_PAGE, visible.length) + ' of ' + visible.length + '</span>' +
|
|
77
80
|
'<div class="pr-pager-btns">' +
|
|
@@ -81,6 +84,7 @@ function renderMeetings(meetings) {
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
if (archived.length > 0) {
|
|
87
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: composed from internal archived count and fixed toggle label (no user data flows in)
|
|
84
88
|
el.insertAdjacentHTML('beforeend', '<div style="text-align:center;margin-top:8px"><button class="pr-pager-btn" style="font-size:10px" onclick="_toggleArchivedMeetings()">' +
|
|
85
89
|
(_showArchived ? 'Hide' : 'Show') + ' ' + archived.length + ' archived</button></div>');
|
|
86
90
|
}
|
|
@@ -213,6 +217,7 @@ function _renderMeetingDetail(m) {
|
|
|
213
217
|
document.getElementById('modal-title').textContent = 'Meeting: ' + m.title;
|
|
214
218
|
var body = document.getElementById('modal-body');
|
|
215
219
|
var scrollTop = body.scrollTop;
|
|
220
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled markdown fields before assembling HTML (see dashboard/js/utils.js); ids and labels use escHtml()
|
|
216
221
|
body.innerHTML = html;
|
|
217
222
|
body.style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
218
223
|
body.style.whiteSpace = 'normal';
|
|
@@ -274,6 +279,7 @@ function openCreateMeetingModal() {
|
|
|
274
279
|
const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
|
|
275
280
|
|
|
276
281
|
document.getElementById('modal-title').textContent = 'New Team Meeting';
|
|
282
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: agent id, name, role)
|
|
277
283
|
document.getElementById('modal-body').innerHTML =
|
|
278
284
|
'<div style="display:flex;flex-direction:column;gap:10px">' +
|
|
279
285
|
'<label style="color:var(--text);font-size:var(--text-md)">Title<input id="mtg-title" style="' + inputStyle + '" placeholder="e.g. Should we add SQLite?"></label>' +
|
|
@@ -9,6 +9,7 @@ function renderProjects(projects) {
|
|
|
9
9
|
'<span onclick="openScanProjectsModal()" style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--blue);font-weight:500;cursor:pointer;border-style:dashed;font-size:10px">Scan</span>';
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: project name, path, branch metadata)
|
|
12
13
|
list.innerHTML = visible.map(p =>
|
|
13
14
|
'<span data-project="' + escHtml(p.name) + '" title="' + escHtml(p.path || '') + '" style="display:inline-flex;align-items:center;gap:6px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--blue);font-weight:500;cursor:help">' +
|
|
14
15
|
escHtml(p.name) +
|
|
@@ -137,6 +138,7 @@ function renderMcpServers(servers) {
|
|
|
137
138
|
el.innerHTML = '<p class="empty">No MCP servers found. Add them via <code>claude mcp add</code> / <code>copilot mcp add</code>, install a plugin that ships one, or drop a <code>.mcp.json</code> into a registered project — they\'ll appear here automatically.</p>';
|
|
138
139
|
return;
|
|
139
140
|
}
|
|
141
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: server source, status, args, command, name)
|
|
140
142
|
el.innerHTML = '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px">' +
|
|
141
143
|
servers.map(s =>
|
|
142
144
|
'<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.source, s.status, s.args || s.command].filter(Boolean).join(' · ')) + '">' +
|
|
@@ -202,6 +204,7 @@ function renderMetrics(metrics) {
|
|
|
202
204
|
'</tr>';
|
|
203
205
|
}
|
|
204
206
|
html += '</tbody></table>';
|
|
207
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: agent id)
|
|
205
208
|
el.innerHTML = html;
|
|
206
209
|
renderLlmPerf(metrics);
|
|
207
210
|
renderTokenUsage(metrics);
|
|
@@ -250,6 +253,7 @@ function renderLlmPerf(metrics) {
|
|
|
250
253
|
}
|
|
251
254
|
}
|
|
252
255
|
html += '</tbody></table>';
|
|
256
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: LLM call type)
|
|
253
257
|
el.innerHTML = html;
|
|
254
258
|
}
|
|
255
259
|
|
|
@@ -401,12 +405,14 @@ function renderTokenUsage(metrics) {
|
|
|
401
405
|
html += '</tbody></table>';
|
|
402
406
|
}
|
|
403
407
|
|
|
408
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: agent id, model label, engine category)
|
|
404
409
|
el.innerHTML = html;
|
|
405
410
|
}
|
|
406
411
|
|
|
407
412
|
|
|
408
413
|
async function openScanProjectsModal() {
|
|
409
414
|
document.getElementById('modal-title').textContent = 'Scan for Projects';
|
|
415
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: default scan path)
|
|
410
416
|
document.getElementById('modal-body').innerHTML =
|
|
411
417
|
'<div style="display:flex;flex-direction:column;gap:12px">' +
|
|
412
418
|
'<div style="display:flex;gap:8px;align-items:flex-end">' +
|
|
@@ -439,8 +445,10 @@ async function _runProjectScan() {
|
|
|
439
445
|
body: JSON.stringify({ path: scanPath, depth: Number(depth) })
|
|
440
446
|
});
|
|
441
447
|
var data = await res.json();
|
|
448
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: scan error)
|
|
442
449
|
if (!res.ok) { resultsEl.innerHTML = '<span style="color:var(--red)">Error: ' + escHtml(data.error) + '</span>'; return; }
|
|
443
450
|
var repos = data.repos || [];
|
|
451
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: scan path)
|
|
444
452
|
if (repos.length === 0) { resultsEl.innerHTML = '<span style="color:var(--muted)">No git repos found in ' + escHtml(scanPath) + '</span>'; return; }
|
|
445
453
|
|
|
446
454
|
var html = '<div style="margin-bottom:8px;font-size:11px;color:var(--muted)">' + repos.length + ' repos found — select to add:</div>';
|
|
@@ -467,8 +475,10 @@ async function _runProjectScan() {
|
|
|
467
475
|
'</div>' +
|
|
468
476
|
'<button onclick="_addSelectedProjects()" style="padding:6px 16px;background:var(--green);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Add Selected</button>' +
|
|
469
477
|
'</div>';
|
|
478
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: repo host, name, path, description)
|
|
470
479
|
resultsEl.innerHTML = html;
|
|
471
480
|
window._scanRepos = repos;
|
|
481
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: error message)
|
|
472
482
|
} catch (e) { resultsEl.innerHTML = '<span style="color:var(--red)">Error: ' + escHtml(e.message) + '</span>'; }
|
|
473
483
|
}
|
|
474
484
|
|
|
@@ -640,6 +650,7 @@ async function renderKeepProcesses() {
|
|
|
640
650
|
// append so each mount needs its own copy).
|
|
641
651
|
for (const m of liveMounts) {
|
|
642
652
|
if (m.countEl) m.countEl.textContent = countText;
|
|
653
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: fetch error, agent id, reason, file path, work item id, purpose, cwd)
|
|
643
654
|
const frag = document.createRange().createContextualFragment(html);
|
|
644
655
|
m.root.replaceChildren(frag);
|
|
645
656
|
}
|
|
@@ -10,6 +10,7 @@ function renderPinned(entries) {
|
|
|
10
10
|
}
|
|
11
11
|
// Store entries for click-to-view (full content available in status response)
|
|
12
12
|
window._pinnedEntries = entries;
|
|
13
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml()/renderMd() (fields: title, content)
|
|
13
14
|
el.innerHTML = entries.map((e, i) =>
|
|
14
15
|
'<div class="pinned-card" data-file="pinned:' + escHtml(e.title) + '" style="padding:8px 12px;margin-bottom:6px;background:var(--surface2);border-left:3px solid ' +
|
|
15
16
|
(e.level === 'critical' ? 'var(--red)' : e.level === 'warning' ? 'var(--yellow)' : 'var(--blue)') +
|
|
@@ -90,6 +91,7 @@ function openPinnedView(idx) {
|
|
|
90
91
|
const entry = (window._pinnedEntries || [])[idx];
|
|
91
92
|
if (!entry) return;
|
|
92
93
|
document.getElementById('modal-title').textContent = entry.title;
|
|
94
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
|
|
93
95
|
document.getElementById('modal-body').innerHTML = renderMd(entry.content);
|
|
94
96
|
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
95
97
|
document.getElementById('modal-body').style.whiteSpace = 'normal';
|