orquesta-agent 0.2.57 → 0.2.59

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.
@@ -1,27 +1,72 @@
1
- // Orquesta Agent Manager — Frontend
1
+ // Orquesta Agent Manager
2
2
 
3
3
  const API = '/api'
4
4
  let agents = []
5
5
  let systemInfo = null
6
6
  let editingAgentId = null
7
- let openLogsId = null
8
- let logsIntervals = new Map()
7
+ let openAgentId = null
8
+ let openPanel = 'logs'
9
+ let logSources = new Map() // agentId → EventSource
9
10
 
10
- // ── Icons (inline SVG) ─────────────────────────────────────────────
11
+ // ── SVG Icons ──────────────────────────────────────────────────────
11
12
 
12
- const ICONS = {
13
- play: '<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21"/></svg>',
13
+ const I = {
14
+ server: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1"/><circle cx="6" cy="18" r="1"/></svg>',
15
+ play: '<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="6 3 20 12 6 21"/></svg>',
14
16
  stop: '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="1"/></svg>',
15
- restart: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg>',
16
- logs: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>',
17
+ restart: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>',
18
+ terminal: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>',
19
+ file: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
17
20
  edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
18
21
  trash: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
19
22
  link: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>',
20
- chevDown: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>',
21
- chevUp: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg>',
23
+ x: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
24
+ clear: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg>',
22
25
  }
23
26
 
24
- // ── System Health ──────────────────────────────────────────────────
27
+ // ── Tab switching ──────────────────────────────────────────────────
28
+
29
+ function switchTab(tab) {
30
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'))
31
+ document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'))
32
+ document.getElementById('tab-' + tab)?.classList.add('active')
33
+ document.querySelector(`[data-tab="${tab}"]`)?.classList.add('active')
34
+ if (tab === 'health') loadHealth()
35
+ }
36
+
37
+ // ── System Info ────────────────────────────────────────────────────
38
+
39
+ async function loadSystemInfo() {
40
+ try {
41
+ const res = await fetch(`${API}/system`)
42
+ systemInfo = await res.json()
43
+ document.getElementById('version').textContent = `v${systemInfo.version}`
44
+
45
+ // Claude
46
+ const cd = document.getElementById('claude-dot')
47
+ const cs = document.getElementById('claude-status')
48
+ if (systemInfo.hasClaudeCli && systemInfo.claudeAuth?.authenticated) {
49
+ cd.className = 'cli-dot on'; cs.textContent = systemInfo.claudeAuth.method
50
+ } else if (systemInfo.hasClaudeCli) {
51
+ cd.className = 'cli-dot warn'; cs.textContent = 'no auth'
52
+ } else {
53
+ cd.className = 'cli-dot'; cs.textContent = 'missing'
54
+ }
55
+
56
+ // Orquesta
57
+ const od = document.getElementById('orquesta-dot')
58
+ const os2 = document.getElementById('orquesta-status')
59
+ if (systemInfo.hasOrquestaCli) {
60
+ od.className = 'cli-dot on'; os2.textContent = 'ready'
61
+ } else {
62
+ od.className = 'cli-dot'; os2.textContent = 'missing'
63
+ }
64
+
65
+ document.getElementById('node-status').textContent = systemInfo.nodeVersion
66
+ } catch {}
67
+ }
68
+
69
+ // ── Health ─────────────────────────────────────────────────────────
25
70
 
26
71
  async function loadHealth() {
27
72
  try {
@@ -29,82 +74,32 @@ async function loadHealth() {
29
74
  if (!res.ok) return
30
75
  const h = await res.json()
31
76
 
32
- // CPU
33
77
  const cpuPct = h.cpu.usage
34
- document.getElementById('cpu-value').innerHTML = `${cpuPct}<small>%</small>`
78
+ document.getElementById('cpu-value').textContent = cpuPct + '%'
35
79
  setBar('cpu-bar', cpuPct)
36
80
  document.getElementById('cpu-detail').textContent = `${h.cpu.count} cores`
37
81
 
38
- // Memory
39
82
  const memPct = Math.round((h.memory.used / h.memory.total) * 100)
40
- document.getElementById('mem-value').innerHTML = `${memPct}<small>%</small>`
83
+ document.getElementById('mem-value').textContent = memPct + '%'
41
84
  setBar('mem-bar', memPct)
42
85
  document.getElementById('mem-detail').textContent = `${fmtBytes(h.memory.used)} / ${fmtBytes(h.memory.total)}`
43
86
 
44
- // Disk
45
87
  if (h.disk.total > 0) {
46
88
  const diskPct = Math.round((h.disk.used / h.disk.total) * 100)
47
- document.getElementById('disk-value').innerHTML = `${diskPct}<small>%</small>`
89
+ document.getElementById('disk-value').textContent = diskPct + '%'
48
90
  setBar('disk-bar', diskPct)
49
- document.getElementById('disk-detail').textContent = `${fmtBytes(h.disk.free)} free of ${fmtBytes(h.disk.total)}`
50
- } else {
51
- document.getElementById('disk-value').innerHTML = `N/A`
91
+ document.getElementById('disk-detail').textContent = `${fmtBytes(h.disk.free)} free`
52
92
  }
53
93
 
54
- // Uptime
55
94
  document.getElementById('uptime-value').textContent = fmtUptime(h.uptime * 1000)
56
95
  document.getElementById('hostname-detail').textContent = h.hostname
57
- } catch { /* ignore */ }
96
+ } catch {}
58
97
  }
59
98
 
60
99
  function setBar(id, pct) {
61
100
  const el = document.getElementById(id)
62
- el.style.width = `${Math.min(pct, 100)}%`
63
- el.className = 'health-bar-fill ' + (pct > 90 ? 'red' : pct > 70 ? 'yellow' : 'green')
64
- }
65
-
66
- // ── System Info ────────────────────────────────────────────────────
67
-
68
- async function loadSystemInfo() {
69
- try {
70
- const res = await fetch(`${API}/system`)
71
- systemInfo = await res.json()
72
-
73
- document.getElementById('version').textContent = `v${systemInfo.version}`
74
-
75
- // Claude CLI mode
76
- const claudeCard = document.getElementById('mode-claude')
77
- const claudeDot = document.getElementById('claude-dot')
78
- const claudeLabel = document.getElementById('claude-label')
79
- if (systemInfo.hasClaudeCli && systemInfo.claudeAuth?.authenticated) {
80
- claudeDot.className = 'dot on'
81
- claudeLabel.textContent = `Ready (${systemInfo.claudeAuth.method})`
82
- claudeCard.classList.add('active')
83
- } else if (systemInfo.hasClaudeCli) {
84
- claudeDot.className = 'dot warn'
85
- claudeLabel.textContent = 'Installed but not authenticated'
86
- } else {
87
- claudeDot.className = 'dot off'
88
- claudeLabel.textContent = 'Not installed'
89
- }
90
-
91
- // Orquesta CLI mode (Batuta proxy)
92
- const orqCard = document.getElementById('mode-orquesta')
93
- const orqDot = document.getElementById('orquesta-dot')
94
- const orqLabel = document.getElementById('orquesta-label')
95
- if (systemInfo.hasOrquestaCli) {
96
- orqDot.className = 'dot on'
97
- orqLabel.textContent = 'Ready (Claude, GPT-4o, DeepSeek, Gemini...)'
98
- orqCard.classList.add('active')
99
- } else {
100
- orqDot.className = 'dot off'
101
- orqLabel.textContent = 'Not installed — npm i -g orquesta-cli'
102
- }
103
-
104
- // Node
105
- document.getElementById('node-dot').className = 'dot on'
106
- document.getElementById('node-label').textContent = systemInfo.nodeVersion
107
- } catch { /* ignore */ }
101
+ el.style.width = Math.min(pct, 100) + '%'
102
+ el.className = 'health-bar-fill' + (pct > 90 ? ' red' : pct > 70 ? ' yellow' : '')
108
103
  }
109
104
 
110
105
  // ── Agents ─────────────────────────────────────────────────────────
@@ -114,79 +109,77 @@ async function loadAgents() {
114
109
  const res = await fetch(`${API}/agents`)
115
110
  agents = await res.json()
116
111
  renderAgents()
117
- } catch { /* ignore */ }
112
+ } catch {}
118
113
  }
119
114
 
120
115
  function renderAgents() {
121
- const container = document.getElementById('agents-container')
122
- const emptyState = document.getElementById('empty-state')
116
+ const c = document.getElementById('agents-container')
123
117
  const countEl = document.getElementById('agent-count')
124
118
 
125
119
  if (agents.length === 0) {
126
- emptyState.style.display = ''
127
- container.innerHTML = ''
128
- container.appendChild(emptyState)
129
- countEl.textContent = ''
120
+ countEl.textContent = 'No agents running'
121
+ c.innerHTML = `<div class="empty-state">
122
+ ${I.server}
123
+ <h3>No agents configured</h3>
124
+ <p>Add an agent to connect your project to Orquesta</p>
125
+ <button class="btn btn-primary" onclick="openAddAgentModal()">Add Agent</button>
126
+ </div>`
130
127
  return
131
128
  }
132
129
 
133
- emptyState.style.display = 'none'
134
130
  const running = agents.filter(a => a.status === 'running').length
135
- countEl.textContent = `(${running}/${agents.length} running)`
131
+ countEl.textContent = `${running} of ${agents.length} online`
136
132
 
137
- container.innerHTML = agents.map(a => {
138
- const isLogsOpen = openLogsId === a.id
139
- return `
140
- <div class="agent-card ${a.status}" id="card-${a.id}">
133
+ c.innerHTML = '<div class="agents-grid">' + agents.map(a => {
134
+ const isOpen = openAgentId === a.id
135
+ const isDiscovered = a.id.startsWith('discovered-')
136
+ const displayName = isDiscovered ? a.workingDir.split('/').pop() || a.name : a.name
137
+
138
+ return `<div class="agent-card" id="card-${a.id}">
141
139
  <div class="agent-card-header">
142
- <div class="agent-card-left">
143
- <span class="agent-status-dot ${a.status}"></span>
144
- <div>
145
- <div class="agent-name">${esc(a.name)}</div>
146
- <a class="agent-project-link" href="https://orquesta.live/dashboard/projects" target="_blank" rel="noopener">
147
- ${ICONS.link} Open in Dashboard
148
- </a>
140
+ <div class="agent-icon ${a.status}">${I.server}</div>
141
+ <div class="agent-info">
142
+ <div class="agent-name-row">
143
+ <span class="agent-name">${esc(displayName)}</span>
144
+ <span class="agent-badge ${a.status}"><span class="badge-dot ${a.status === 'running' ? 'pulse' : ''}"></span>${a.status}</span>
145
+ </div>
146
+ <div class="agent-meta">
147
+ <span>${esc(a.workingDir)}</span>
148
+ ${a.pid ? `<span>PID <span class="meta-val">${a.pid}</span></span>` : ''}
149
+ ${a.uptime ? `<span>${fmtUptime(a.uptime)}</span>` : ''}
149
150
  </div>
150
151
  </div>
151
- <div class="agent-card-right">
152
+ <div class="agent-actions">
152
153
  ${a.status === 'running' ? `
153
- <button class="btn btn-sm" onclick="stopAgent('${a.id}')" title="Stop">${ICONS.stop} <span class="btn-label">Stop</span></button>
154
- <button class="btn btn-sm" onclick="restartAgent('${a.id}')" title="Restart">${ICONS.restart} <span class="btn-label">Restart</span></button>
154
+ <button class="btn btn-sm" onclick="stopAgent('${a.id}')" title="Stop">${I.stop}</button>
155
+ <button class="btn btn-sm" onclick="restartAgent('${a.id}')" title="Restart">${I.restart}</button>
155
156
  ` : `
156
- <button class="btn btn-sm btn-success" onclick="startAgent('${a.id}')" title="Start">${ICONS.play} <span class="btn-label">Start</span></button>
157
+ <button class="btn btn-sm btn-primary" onclick="startAgent('${a.id}')" title="Start">${I.play}</button>
157
158
  `}
158
- <button class="btn btn-sm" onclick="toggleLogs('${a.id}')" title="Logs">${isLogsOpen ? ICONS.chevUp : ICONS.logs}</button>
159
- <button class="btn btn-sm" onclick="editAgent('${a.id}')" title="Edit">${ICONS.edit}</button>
160
- <button class="btn btn-sm btn-danger" onclick="deleteAgent('${a.id}', '${esc(a.name)}')" title="Delete">${ICONS.trash}</button>
159
+ <button class="btn btn-sm ${isOpen ? 'active' : ''}" onclick="togglePanel('${a.id}')" title="Logs">${I.terminal}</button>
160
+ ${!isDiscovered ? `
161
+ <button class="btn btn-icon btn-sm" onclick="editAgent('${a.id}')" title="Edit">${I.edit}</button>
162
+ <button class="btn btn-icon btn-sm btn-danger" onclick="deleteAgent('${a.id}','${esc(displayName)}')" title="Delete">${I.trash}</button>
163
+ ` : ''}
161
164
  </div>
162
165
  </div>
163
-
164
- <div class="agent-card-meta">
165
- <div class="meta-item"><span class="meta-label">Dir</span><span class="meta-value">${esc(a.workingDir)}</span></div>
166
- <div class="meta-item"><span class="meta-label">CLI</span><span class="meta-value">${a.cliPreference}</span></div>
167
- <div class="meta-item"><span class="meta-label">Mode</span><span class="meta-value">${a.permissionMode}</span></div>
168
- ${a.pid ? `<div class="meta-item"><span class="meta-label">PID</span><span class="meta-value">${a.pid}</span></div>` : ''}
169
- ${a.uptime ? `<div class="meta-item"><span class="meta-label">Uptime</span><span class="meta-value">${fmtUptime(a.uptime)}</span></div>` : ''}
170
- ${a.error ? `<div class="meta-item"><span class="meta-label" style="color:var(--red)">Error</span><span class="meta-value" style="color:var(--red)">${esc(a.error)}</span></div>` : ''}
171
- </div>
172
-
173
- <div class="agent-logs ${isLogsOpen ? 'open' : ''}" id="logs-${a.id}">
174
- <div class="agent-logs-header">
175
- <span>Live Logs</span>
176
- <div style="display:flex;gap:6px">
177
- <button class="btn btn-sm" onclick="refreshLogs('${a.id}')">Refresh</button>
178
- <button class="btn btn-sm btn-danger" onclick="clearLogs('${a.id}')">Clear</button>
166
+ <div class="agent-tabs ${isOpen ? 'open' : ''}" id="tabs-${a.id}">
167
+ <div class="agent-tabs-bar">
168
+ <button class="agent-tab active" onclick="switchPanel('${a.id}','logs')">Logs</button>
169
+ <div class="agent-tabs-actions">
170
+ <button class="btn btn-sm btn-icon" onclick="clearLogs('${a.id}')" title="Clear">${I.clear}</button>
171
+ <button class="btn btn-sm btn-icon" onclick="togglePanel('${a.id}')" title="Close">${I.x}</button>
179
172
  </div>
180
173
  </div>
181
- <pre class="logs-content" id="logs-content-${a.id}"></pre>
174
+ <div class="agent-panel active" id="panel-logs-${a.id}">
175
+ <div class="log-viewer" id="logs-${a.id}"></div>
176
+ </div>
182
177
  </div>
183
178
  </div>`
184
- }).join('')
179
+ }).join('') + '</div>'
185
180
 
186
- // Restore open logs
187
- if (openLogsId) {
188
- refreshLogs(openLogsId)
189
- }
181
+ // Reconnect SSE for open panel
182
+ if (openAgentId) connectLogStream(openAgentId)
190
183
  }
191
184
 
192
185
  // ── Agent Actions ──────────────────────────────────────────────────
@@ -195,7 +188,7 @@ async function startAgent(id) {
195
188
  try {
196
189
  const res = await fetch(`${API}/agents/${id}/start`, { method: 'POST' })
197
190
  if (!res.ok) { const d = await res.json(); alert(d.error); return }
198
- setTimeout(loadAgents, 1000)
191
+ setTimeout(loadAgents, 1500)
199
192
  } catch (e) { alert(e.message) }
200
193
  }
201
194
 
@@ -216,72 +209,61 @@ async function restartAgent(id) {
216
209
  }
217
210
 
218
211
  async function deleteAgent(id, name) {
219
- if (!confirm(`Delete agent "${name}"? This will stop it and remove all configuration.`)) return
212
+ if (!confirm(`Delete agent "${name}"?`)) return
220
213
  try {
221
214
  const res = await fetch(`${API}/agents/${id}`, { method: 'DELETE' })
222
215
  if (!res.ok) { const d = await res.json(); alert(d.error); return }
223
- if (openLogsId === id) openLogsId = null
216
+ if (openAgentId === id) { disconnectLogStream(id); openAgentId = null }
224
217
  loadAgents()
225
218
  } catch (e) { alert(e.message) }
226
219
  }
227
220
 
228
- // ── Inline Logs ────────────────────────────────────────────────────
221
+ // ── Log Panel ──────────────────────────────────────────────────────
229
222
 
230
- function toggleLogs(id) {
231
- if (openLogsId === id) {
232
- openLogsId = null
233
- const el = document.getElementById(`logs-${id}`)
234
- if (el) el.classList.remove('open')
235
- stopLogsPolling(id)
223
+ function togglePanel(id) {
224
+ if (openAgentId === id) {
225
+ disconnectLogStream(id)
226
+ openAgentId = null
236
227
  } else {
237
- // Close previous
238
- if (openLogsId) {
239
- const prev = document.getElementById(`logs-${openLogsId}`)
240
- if (prev) prev.classList.remove('open')
241
- stopLogsPolling(openLogsId)
242
- }
243
- openLogsId = id
244
- const el = document.getElementById(`logs-${id}`)
245
- if (el) el.classList.add('open')
246
- refreshLogs(id)
247
- startLogsPolling(id)
228
+ if (openAgentId) disconnectLogStream(openAgentId)
229
+ openAgentId = id
230
+ connectLogStream(id)
248
231
  }
249
- // Re-render buttons (toggle icon)
250
232
  renderAgents()
251
233
  }
252
234
 
253
- function startLogsPolling(id) {
254
- stopLogsPolling(id)
255
- const interval = setInterval(() => refreshLogs(id), 2000)
256
- logsIntervals.set(id, interval)
257
- }
258
-
259
- function stopLogsPolling(id) {
260
- const interval = logsIntervals.get(id)
261
- if (interval) {
262
- clearInterval(interval)
263
- logsIntervals.delete(id)
235
+ function connectLogStream(id) {
236
+ disconnectLogStream(id)
237
+ const es = new EventSource(`${API}/agents/${id}/logs/stream`)
238
+ es.onmessage = (e) => {
239
+ try {
240
+ const { logs } = JSON.parse(e.data)
241
+ const el = document.getElementById('logs-' + id)
242
+ if (el) {
243
+ el.textContent = logs.join('\n')
244
+ el.scrollTop = el.scrollHeight
245
+ }
246
+ } catch {}
247
+ }
248
+ es.onerror = () => {
249
+ // Reconnect on error after 3s
250
+ disconnectLogStream(id)
251
+ setTimeout(() => {
252
+ if (openAgentId === id) connectLogStream(id)
253
+ }, 3000)
264
254
  }
255
+ logSources.set(id, es)
265
256
  }
266
257
 
267
- async function refreshLogs(id) {
268
- try {
269
- const res = await fetch(`${API}/agents/${id}/logs?lines=200`)
270
- const data = await res.json()
271
- const el = document.getElementById(`logs-content-${id}`)
272
- if (el) {
273
- el.textContent = data.logs.join('\n') || 'No logs yet'
274
- el.scrollTop = el.scrollHeight
275
- }
276
- } catch { /* ignore */ }
258
+ function disconnectLogStream(id) {
259
+ const es = logSources.get(id)
260
+ if (es) { es.close(); logSources.delete(id) }
277
261
  }
278
262
 
279
263
  async function clearLogs(id) {
280
- if (!confirm('Clear all logs for this agent?')) return
281
264
  try {
282
265
  await fetch(`${API}/agents/${id}/logs`, { method: 'DELETE' })
283
- refreshLogs(id)
284
- } catch { /* ignore */ }
266
+ } catch {}
285
267
  }
286
268
 
287
269
  // ── Modal ──────────────────────────────────────────────────────────
@@ -304,7 +286,6 @@ function editAgent(id) {
304
286
  document.getElementById('agent-token').value = a.token
305
287
  document.getElementById('agent-cli-preference').value = a.cliPreference
306
288
  document.getElementById('agent-permission-mode').value = a.permissionMode
307
- document.getElementById('agent-auto-start').checked = a.autoStart
308
289
  document.getElementById('agent-auto-pull').checked = a.autoPull
309
290
  document.getElementById('agent-modal').classList.add('active')
310
291
  }
@@ -314,18 +295,17 @@ function closeAgentModal() {
314
295
  editingAgentId = null
315
296
  }
316
297
 
317
- async function saveAgent(event) {
318
- event.preventDefault()
298
+ async function saveAgent(e) {
299
+ e.preventDefault()
319
300
  const body = {
320
301
  name: document.getElementById('agent-name').value,
321
302
  workingDir: document.getElementById('agent-working-dir').value,
322
303
  token: document.getElementById('agent-token').value,
323
304
  cliPreference: document.getElementById('agent-cli-preference').value,
324
305
  permissionMode: document.getElementById('agent-permission-mode').value,
325
- autoStart: document.getElementById('agent-auto-start').checked,
306
+ autoStart: false,
326
307
  autoPull: document.getElementById('agent-auto-pull').checked,
327
308
  }
328
-
329
309
  try {
330
310
  const url = editingAgentId ? `${API}/agents/${editingAgentId}` : `${API}/agents`
331
311
  const method = editingAgentId ? 'PUT' : 'POST'
@@ -333,34 +313,25 @@ async function saveAgent(event) {
333
313
  if (!res.ok) { const d = await res.json(); alert(d.error); return }
334
314
  closeAgentModal()
335
315
  loadAgents()
336
- } catch (e) { alert(e.message) }
316
+ } catch (e2) { alert(e2.message) }
337
317
  }
338
318
 
339
319
  // ── Utilities ──────────────────────────────────────────────────────
340
320
 
341
- function esc(text) {
342
- const d = document.createElement('div')
343
- d.textContent = text || ''
344
- return d.innerHTML
345
- }
321
+ function esc(t) { const d = document.createElement('div'); d.textContent = t || ''; return d.innerHTML }
346
322
 
347
323
  function fmtUptime(ms) {
348
- const s = Math.floor(ms / 1000)
349
- const m = Math.floor(s / 60)
350
- const h = Math.floor(m / 60)
351
- const d = Math.floor(h / 24)
352
- if (d > 0) return `${d}d ${h % 24}h`
353
- if (h > 0) return `${h}h ${m % 60}m`
354
- if (m > 0) return `${m}m ${s % 60}s`
355
- return `${s}s`
324
+ const s = Math.floor(ms / 1000), m = Math.floor(s / 60), h = Math.floor(m / 60), d = Math.floor(h / 24)
325
+ if (d > 0) return d + 'd ' + (h % 24) + 'h'
326
+ if (h > 0) return h + 'h ' + (m % 60) + 'm'
327
+ if (m > 0) return m + 'm'
328
+ return s + 's'
356
329
  }
357
330
 
358
- function fmtBytes(bytes) {
359
- if (bytes === 0) return '0 B'
360
- const k = 1024
361
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
362
- const i = Math.floor(Math.log(bytes) / Math.log(k))
363
- return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
331
+ function fmtBytes(b) {
332
+ if (!b) return '0 B'
333
+ const k = 1024, s = ['B','KB','MB','GB','TB'], i = Math.floor(Math.log(b) / Math.log(k))
334
+ return (b / Math.pow(k, i)).toFixed(1) + ' ' + s[i]
364
335
  }
365
336
 
366
337
  // ── Init ───────────────────────────────────────────────────────────
@@ -369,26 +340,13 @@ document.addEventListener('DOMContentLoaded', () => {
369
340
  loadSystemInfo()
370
341
  loadHealth()
371
342
  loadAgents()
372
-
373
- document.getElementById('refresh-btn').addEventListener('click', () => {
374
- const icon = document.getElementById('refresh-icon')
375
- icon.classList.add('spinning')
376
- Promise.all([loadSystemInfo(), loadHealth(), loadAgents()]).then(() => {
377
- setTimeout(() => icon.classList.remove('spinning'), 500)
378
- })
379
- })
380
-
381
- document.getElementById('add-agent-btn').addEventListener('click', openAddAgentModal)
382
343
  document.getElementById('agent-form').addEventListener('submit', saveAgent)
383
-
384
- // Auto-refresh
385
344
  setInterval(loadAgents, 5000)
386
- setInterval(loadHealth, 10000)
345
+ setInterval(loadHealth, 15000)
387
346
  })
388
347
 
389
- // Close modals on background click
390
348
  window.addEventListener('click', (e) => {
391
- if (e.target.classList.contains('modal')) {
349
+ if (e.target.classList.contains('modal-overlay')) {
392
350
  e.target.classList.remove('active')
393
351
  }
394
352
  })