@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.
@@ -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();
@@ -158,6 +158,7 @@ function renderFre(statusOrProjects) {
158
158
  return ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;' })[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;
@@ -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';
@@ -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(' ');
@@ -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
  }
@@ -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: 1,
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
- nameEl.replaceChildren(
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)">&#x2713; 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">&#x26A0;&#xFE0F; 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">&#x26A0;&#xFE0F; 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">&#x26A0;&#xFE0F; 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">&#x26A0;&#xFE0F; 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';