orquesta-agent 0.2.53 → 0.2.54

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,248 +1,318 @@
1
- // Orquesta Agent Manager Frontend
1
+ // Orquesta Agent Manager Frontend
2
2
 
3
- const API_BASE = '/api'
3
+ const API = '/api'
4
4
  let agents = []
5
5
  let systemInfo = null
6
6
  let editingAgentId = null
7
- let logsAgentId = null
8
- let logsInterval = null
7
+ let openLogsId = null
8
+ let logsIntervals = new Map()
9
+
10
+ // ── Icons (inline SVG) ─────────────────────────────────────────────
11
+
12
+ const ICONS = {
13
+ play: '<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21"/></svg>',
14
+ 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
+ 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
+ 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
+ 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>',
22
+ }
23
+
24
+ // ── System Health ──────────────────────────────────────────────────
25
+
26
+ async function loadHealth() {
27
+ try {
28
+ const res = await fetch(`${API}/system/health`)
29
+ if (!res.ok) return
30
+ const h = await res.json()
31
+
32
+ // CPU
33
+ const cpuPct = h.cpu.usage
34
+ document.getElementById('cpu-value').innerHTML = `${cpuPct}<small>%</small>`
35
+ setBar('cpu-bar', cpuPct)
36
+ document.getElementById('cpu-detail').textContent = `${h.cpu.count} cores`
37
+
38
+ // Memory
39
+ const memPct = Math.round((h.memory.used / h.memory.total) * 100)
40
+ document.getElementById('mem-value').innerHTML = `${memPct}<small>%</small>`
41
+ setBar('mem-bar', memPct)
42
+ document.getElementById('mem-detail').textContent = `${fmtBytes(h.memory.used)} / ${fmtBytes(h.memory.total)}`
43
+
44
+ // Disk
45
+ if (h.disk.total > 0) {
46
+ const diskPct = Math.round((h.disk.used / h.disk.total) * 100)
47
+ document.getElementById('disk-value').innerHTML = `${diskPct}<small>%</small>`
48
+ 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`
52
+ }
53
+
54
+ // Uptime
55
+ document.getElementById('uptime-value').textContent = fmtUptime(h.uptime * 1000)
56
+ document.getElementById('hostname-detail').textContent = h.hostname
57
+ } catch { /* ignore */ }
58
+ }
59
+
60
+ function setBar(id, pct) {
61
+ 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 ────────────────────────────────────────────────────
9
67
 
10
- // Load system info
11
68
  async function loadSystemInfo() {
12
69
  try {
13
- const res = await fetch(`${API_BASE}/system`)
70
+ const res = await fetch(`${API}/system`)
14
71
  systemInfo = await res.json()
15
72
 
16
- // Update UI
17
73
  document.getElementById('version').textContent = `v${systemInfo.version}`
18
- document.getElementById('platform').textContent = systemInfo.platform
19
-
20
- // Claude CLI status
21
- const claudeStatus = document.getElementById('claude-status')
22
- if (systemInfo.hasClaudeCli) {
23
- if (systemInfo.claudeAuth?.authenticated) {
24
- claudeStatus.innerHTML = `<span class="badge success">✓ ${systemInfo.claudeAuth.method}</span>`
25
- } else {
26
- claudeStatus.innerHTML = `<span class="badge warning">⚠ Not authenticated</span>`
27
- }
74
+
75
+ // Claude CLI
76
+ const claudeDot = document.getElementById('claude-dot')
77
+ const claudeLabel = document.getElementById('claude-label')
78
+ if (systemInfo.hasClaudeCli && systemInfo.claudeAuth?.authenticated) {
79
+ claudeDot.className = 'dot on'
80
+ claudeLabel.textContent = systemInfo.claudeAuth.method
81
+ } else if (systemInfo.hasClaudeCli) {
82
+ claudeDot.className = 'dot warn'
83
+ claudeLabel.textContent = 'not authenticated'
28
84
  } else {
29
- claudeStatus.innerHTML = `<span class="badge danger">✗ Not installed</span>`
85
+ claudeDot.className = 'dot off'
86
+ claudeLabel.textContent = 'not installed'
30
87
  }
31
88
 
32
- // Orquesta CLI status
33
- const orquestaStatus = document.getElementById('orquesta-status')
89
+ // Orquesta CLI
90
+ const orqDot = document.getElementById('orquesta-dot')
91
+ const orqLabel = document.getElementById('orquesta-label')
34
92
  if (systemInfo.hasOrquestaCli) {
35
- orquestaStatus.innerHTML = `<span class="badge success">✓ Installed</span>`
93
+ orqDot.className = 'dot on'
94
+ orqLabel.textContent = 'installed'
36
95
  } else {
37
- orquestaStatus.innerHTML = `<span class="badge danger">✗ Not installed</span>`
96
+ orqDot.className = 'dot off'
97
+ orqLabel.textContent = 'not installed'
38
98
  }
39
- } catch (err) {
40
- console.error('Failed to load system info:', err)
41
- }
99
+
100
+ // Node
101
+ document.getElementById('node-dot').className = 'dot on'
102
+ document.getElementById('node-label').textContent = systemInfo.nodeVersion
103
+ } catch { /* ignore */ }
42
104
  }
43
105
 
44
- // Load agents
106
+ // ── Agents ─────────────────────────────────────────────────────────
107
+
45
108
  async function loadAgents() {
46
109
  try {
47
- const res = await fetch(`${API_BASE}/agents`)
110
+ const res = await fetch(`${API}/agents`)
48
111
  agents = await res.json()
49
112
  renderAgents()
50
- } catch (err) {
51
- console.error('Failed to load agents:', err)
52
- }
113
+ } catch { /* ignore */ }
53
114
  }
54
115
 
55
- // Render agents list
56
116
  function renderAgents() {
57
117
  const container = document.getElementById('agents-container')
58
118
  const emptyState = document.getElementById('empty-state')
119
+ const countEl = document.getElementById('agent-count')
59
120
 
60
121
  if (agents.length === 0) {
61
- emptyState.style.display = 'block'
122
+ emptyState.style.display = ''
62
123
  container.innerHTML = ''
63
124
  container.appendChild(emptyState)
125
+ countEl.textContent = ''
64
126
  return
65
127
  }
66
128
 
67
129
  emptyState.style.display = 'none'
68
- container.innerHTML = agents.map(agent => `
69
- <div class="agent-card">
70
- <div class="agent-header">
71
- <div class="agent-title">
72
- <span class="status-dot ${agent.status}"></span>
73
- <h3>${escapeHtml(agent.name)}</h3>
130
+ const running = agents.filter(a => a.status === 'running').length
131
+ countEl.textContent = `(${running}/${agents.length} running)`
132
+
133
+ container.innerHTML = agents.map(a => {
134
+ const isLogsOpen = openLogsId === a.id
135
+ return `
136
+ <div class="agent-card ${a.status}" id="card-${a.id}">
137
+ <div class="agent-card-header">
138
+ <div class="agent-card-left">
139
+ <span class="agent-status-dot ${a.status}"></span>
140
+ <div>
141
+ <div class="agent-name">${esc(a.name)}</div>
142
+ <a class="agent-project-link" href="https://orquesta.live/dashboard/projects" target="_blank" rel="noopener">
143
+ ${ICONS.link} Open in Dashboard
144
+ </a>
145
+ </div>
146
+ </div>
147
+ <div class="agent-card-right">
148
+ ${a.status === 'running' ? `
149
+ <button class="btn btn-sm" onclick="stopAgent('${a.id}')" title="Stop">${ICONS.stop} <span class="btn-label">Stop</span></button>
150
+ <button class="btn btn-sm" onclick="restartAgent('${a.id}')" title="Restart">${ICONS.restart} <span class="btn-label">Restart</span></button>
151
+ ` : `
152
+ <button class="btn btn-sm btn-success" onclick="startAgent('${a.id}')" title="Start">${ICONS.play} <span class="btn-label">Start</span></button>
153
+ `}
154
+ <button class="btn btn-sm" onclick="toggleLogs('${a.id}')" title="Logs">${isLogsOpen ? ICONS.chevUp : ICONS.logs}</button>
155
+ <button class="btn btn-sm" onclick="editAgent('${a.id}')" title="Edit">${ICONS.edit}</button>
156
+ <button class="btn btn-sm btn-danger" onclick="deleteAgent('${a.id}', '${esc(a.name)}')" title="Delete">${ICONS.trash}</button>
74
157
  </div>
75
- <span class="badge ${agent.status === 'running' ? 'success' : agent.status === 'error' ? 'danger' : ''}">${agent.status.toUpperCase()}</span>
76
158
  </div>
77
159
 
78
- <div class="agent-info">
79
- <div class="info-row">
80
- <span class="label">Working Dir:</span>
81
- <span class="value">${escapeHtml(agent.workingDir)}</span>
82
- </div>
83
- <div class="info-row">
84
- <span class="label">CLI:</span>
85
- <span class="value">${agent.cliPreference}</span>
86
- </div>
87
- <div class="info-row">
88
- <span class="label">Permission:</span>
89
- <span class="value">${agent.permissionMode}</span>
90
- </div>
91
- ${agent.pid ? `
92
- <div class="info-row">
93
- <span class="label">PID:</span>
94
- <span class="value">${agent.pid}</span>
95
- </div>
96
- ` : ''}
97
- ${agent.uptime ? `
98
- <div class="info-row">
99
- <span class="label">Uptime:</span>
100
- <span class="value">${formatUptime(agent.uptime)}</span>
101
- </div>
102
- ` : ''}
103
- ${agent.error ? `
104
- <div class="info-row">
105
- <span class="label">Error:</span>
106
- <span class="value" style="color: #c62828;">${escapeHtml(agent.error)}</span>
107
- </div>
108
- ` : ''}
160
+ <div class="agent-card-meta">
161
+ <div class="meta-item"><span class="meta-label">Dir</span><span class="meta-value">${esc(a.workingDir)}</span></div>
162
+ <div class="meta-item"><span class="meta-label">CLI</span><span class="meta-value">${a.cliPreference}</span></div>
163
+ <div class="meta-item"><span class="meta-label">Mode</span><span class="meta-value">${a.permissionMode}</span></div>
164
+ ${a.pid ? `<div class="meta-item"><span class="meta-label">PID</span><span class="meta-value">${a.pid}</span></div>` : ''}
165
+ ${a.uptime ? `<div class="meta-item"><span class="meta-label">Uptime</span><span class="meta-value">${fmtUptime(a.uptime)}</span></div>` : ''}
166
+ ${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>` : ''}
109
167
  </div>
110
168
 
111
- <div class="agent-actions">
112
- ${agent.status === 'running' ? `
113
- <button class="btn btn-sm btn-secondary" onclick="stopAgent('${agent.id}')">
114
- <span class="icon">⏹</span> Stop
115
- </button>
116
- <button class="btn btn-sm btn-secondary" onclick="restartAgent('${agent.id}')">
117
- <span class="icon">🔄</span> Restart
118
- </button>
119
- ` : `
120
- <button class="btn btn-sm btn-success" onclick="startAgent('${agent.id}')">
121
- <span class="icon">▶️</span> Start
122
- </button>
123
- `}
124
- <button class="btn btn-sm btn-secondary" onclick="viewLogs('${agent.id}', '${escapeHtml(agent.name)}')">
125
- <span class="icon">📄</span> Logs
126
- </button>
127
- <button class="btn btn-sm btn-secondary" onclick="editAgent('${agent.id}')">
128
- <span class="icon">✏️</span> Edit
129
- </button>
130
- <button class="btn btn-sm btn-danger" onclick="deleteAgent('${agent.id}', '${escapeHtml(agent.name)}')">
131
- <span class="icon">🗑️</span> Delete
132
- </button>
169
+ <div class="agent-logs ${isLogsOpen ? 'open' : ''}" id="logs-${a.id}">
170
+ <div class="agent-logs-header">
171
+ <span>Live Logs</span>
172
+ <div style="display:flex;gap:6px">
173
+ <button class="btn btn-sm" onclick="refreshLogs('${a.id}')">Refresh</button>
174
+ <button class="btn btn-sm btn-danger" onclick="clearLogs('${a.id}')">Clear</button>
175
+ </div>
176
+ </div>
177
+ <pre class="logs-content" id="logs-content-${a.id}"></pre>
133
178
  </div>
134
- </div>
135
- `).join('')
179
+ </div>`
180
+ }).join('')
181
+
182
+ // Restore open logs
183
+ if (openLogsId) {
184
+ refreshLogs(openLogsId)
185
+ }
136
186
  }
137
187
 
138
- // Start agent
188
+ // ── Agent Actions ──────────────────────────────────────────────────
189
+
139
190
  async function startAgent(id) {
140
191
  try {
141
- const res = await fetch(`${API_BASE}/agents/${id}/start`, { method: 'POST' })
142
- const data = await res.json()
143
-
144
- if (!res.ok) {
145
- alert(`Failed to start agent: ${data.error}`)
146
- return
147
- }
148
-
192
+ const res = await fetch(`${API}/agents/${id}/start`, { method: 'POST' })
193
+ if (!res.ok) { const d = await res.json(); alert(d.error); return }
149
194
  setTimeout(loadAgents, 1000)
150
- } catch (err) {
151
- alert(`Failed to start agent: ${err.message}`)
152
- }
195
+ } catch (e) { alert(e.message) }
153
196
  }
154
197
 
155
- // Stop agent
156
198
  async function stopAgent(id) {
157
199
  try {
158
- const res = await fetch(`${API_BASE}/agents/${id}/stop`, { method: 'POST' })
159
- const data = await res.json()
160
-
161
- if (!res.ok) {
162
- alert(`Failed to stop agent: ${data.error}`)
163
- return
164
- }
165
-
200
+ const res = await fetch(`${API}/agents/${id}/stop`, { method: 'POST' })
201
+ if (!res.ok) { const d = await res.json(); alert(d.error); return }
166
202
  setTimeout(loadAgents, 1000)
167
- } catch (err) {
168
- alert(`Failed to stop agent: ${err.message}`)
169
- }
203
+ } catch (e) { alert(e.message) }
170
204
  }
171
205
 
172
- // Restart agent
173
206
  async function restartAgent(id) {
174
207
  try {
175
- const res = await fetch(`${API_BASE}/agents/${id}/restart`, { method: 'POST' })
176
- const data = await res.json()
208
+ const res = await fetch(`${API}/agents/${id}/restart`, { method: 'POST' })
209
+ if (!res.ok) { const d = await res.json(); alert(d.error); return }
210
+ setTimeout(loadAgents, 2000)
211
+ } catch (e) { alert(e.message) }
212
+ }
177
213
 
178
- if (!res.ok) {
179
- alert(`Failed to restart agent: ${data.error}`)
180
- return
181
- }
214
+ async function deleteAgent(id, name) {
215
+ if (!confirm(`Delete agent "${name}"? This will stop it and remove all configuration.`)) return
216
+ try {
217
+ const res = await fetch(`${API}/agents/${id}`, { method: 'DELETE' })
218
+ if (!res.ok) { const d = await res.json(); alert(d.error); return }
219
+ if (openLogsId === id) openLogsId = null
220
+ loadAgents()
221
+ } catch (e) { alert(e.message) }
222
+ }
182
223
 
183
- setTimeout(loadAgents, 2000)
184
- } catch (err) {
185
- alert(`Failed to restart agent: ${err.message}`)
224
+ // ── Inline Logs ────────────────────────────────────────────────────
225
+
226
+ function toggleLogs(id) {
227
+ if (openLogsId === id) {
228
+ openLogsId = null
229
+ const el = document.getElementById(`logs-${id}`)
230
+ if (el) el.classList.remove('open')
231
+ stopLogsPolling(id)
232
+ } else {
233
+ // Close previous
234
+ if (openLogsId) {
235
+ const prev = document.getElementById(`logs-${openLogsId}`)
236
+ if (prev) prev.classList.remove('open')
237
+ stopLogsPolling(openLogsId)
238
+ }
239
+ openLogsId = id
240
+ const el = document.getElementById(`logs-${id}`)
241
+ if (el) el.classList.add('open')
242
+ refreshLogs(id)
243
+ startLogsPolling(id)
186
244
  }
245
+ // Re-render buttons (toggle icon)
246
+ renderAgents()
187
247
  }
188
248
 
189
- // Delete agent
190
- async function deleteAgent(id, name) {
191
- if (!confirm(`Are you sure you want to delete agent "${name}"? This will stop the agent and remove all configuration.`)) {
192
- return
249
+ function startLogsPolling(id) {
250
+ stopLogsPolling(id)
251
+ const interval = setInterval(() => refreshLogs(id), 2000)
252
+ logsIntervals.set(id, interval)
253
+ }
254
+
255
+ function stopLogsPolling(id) {
256
+ const interval = logsIntervals.get(id)
257
+ if (interval) {
258
+ clearInterval(interval)
259
+ logsIntervals.delete(id)
193
260
  }
261
+ }
194
262
 
263
+ async function refreshLogs(id) {
195
264
  try {
196
- const res = await fetch(`${API_BASE}/agents/${id}`, { method: 'DELETE' })
265
+ const res = await fetch(`${API}/agents/${id}/logs?lines=200`)
197
266
  const data = await res.json()
198
-
199
- if (!res.ok) {
200
- alert(`Failed to delete agent: ${data.error}`)
201
- return
267
+ const el = document.getElementById(`logs-content-${id}`)
268
+ if (el) {
269
+ el.textContent = data.logs.join('\n') || 'No logs yet'
270
+ el.scrollTop = el.scrollHeight
202
271
  }
272
+ } catch { /* ignore */ }
273
+ }
203
274
 
204
- loadAgents()
205
- } catch (err) {
206
- alert(`Failed to delete agent: ${err.message}`)
207
- }
275
+ async function clearLogs(id) {
276
+ if (!confirm('Clear all logs for this agent?')) return
277
+ try {
278
+ await fetch(`${API}/agents/${id}/logs`, { method: 'DELETE' })
279
+ refreshLogs(id)
280
+ } catch { /* ignore */ }
208
281
  }
209
282
 
210
- // Open add agent modal
283
+ // ── Modal ──────────────────────────────────────────────────────────
284
+
211
285
  function openAddAgentModal() {
212
286
  editingAgentId = null
213
- document.getElementById('modal-title').textContent = 'Add New Agent'
287
+ document.getElementById('modal-title').textContent = 'Add Agent'
214
288
  document.getElementById('agent-form').reset()
289
+ document.getElementById('agent-auto-pull').checked = true
215
290
  document.getElementById('agent-modal').classList.add('active')
216
291
  }
217
292
 
218
- // Open edit agent modal
219
293
  function editAgent(id) {
220
- const agent = agents.find(a => a.id === id)
221
- if (!agent) return
222
-
294
+ const a = agents.find(x => x.id === id)
295
+ if (!a) return
223
296
  editingAgentId = id
224
297
  document.getElementById('modal-title').textContent = 'Edit Agent'
225
- document.getElementById('agent-name').value = agent.name
226
- document.getElementById('agent-working-dir').value = agent.workingDir
227
- document.getElementById('agent-token').value = agent.token
228
- document.getElementById('agent-cli-preference').value = agent.cliPreference
229
- document.getElementById('agent-permission-mode').value = agent.permissionMode
230
- document.getElementById('agent-auto-start').checked = agent.autoStart
231
- document.getElementById('agent-auto-pull').checked = agent.autoPull
298
+ document.getElementById('agent-name').value = a.name
299
+ document.getElementById('agent-working-dir').value = a.workingDir
300
+ document.getElementById('agent-token').value = a.token
301
+ document.getElementById('agent-cli-preference').value = a.cliPreference
302
+ document.getElementById('agent-permission-mode').value = a.permissionMode
303
+ document.getElementById('agent-auto-start').checked = a.autoStart
304
+ document.getElementById('agent-auto-pull').checked = a.autoPull
232
305
  document.getElementById('agent-modal').classList.add('active')
233
306
  }
234
307
 
235
- // Close agent modal
236
308
  function closeAgentModal() {
237
309
  document.getElementById('agent-modal').classList.remove('active')
238
310
  editingAgentId = null
239
311
  }
240
312
 
241
- // Save agent
242
313
  async function saveAgent(event) {
243
314
  event.preventDefault()
244
-
245
- const formData = {
315
+ const body = {
246
316
  name: document.getElementById('agent-name').value,
247
317
  workingDir: document.getElementById('agent-working-dir').value,
248
318
  token: document.getElementById('agent-token').value,
@@ -253,159 +323,68 @@ async function saveAgent(event) {
253
323
  }
254
324
 
255
325
  try {
256
- let res
257
- if (editingAgentId) {
258
- // Update existing agent
259
- res = await fetch(`${API_BASE}/agents/${editingAgentId}`, {
260
- method: 'PUT',
261
- headers: { 'Content-Type': 'application/json' },
262
- body: JSON.stringify(formData),
263
- })
264
- } else {
265
- // Create new agent
266
- res = await fetch(`${API_BASE}/agents`, {
267
- method: 'POST',
268
- headers: { 'Content-Type': 'application/json' },
269
- body: JSON.stringify(formData),
270
- })
271
- }
272
-
273
- const data = await res.json()
274
-
275
- if (!res.ok) {
276
- alert(`Failed to save agent: ${data.error}`)
277
- return
278
- }
279
-
326
+ const url = editingAgentId ? `${API}/agents/${editingAgentId}` : `${API}/agents`
327
+ const method = editingAgentId ? 'PUT' : 'POST'
328
+ const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
329
+ if (!res.ok) { const d = await res.json(); alert(d.error); return }
280
330
  closeAgentModal()
281
331
  loadAgents()
282
- } catch (err) {
283
- alert(`Failed to save agent: ${err.message}`)
284
- }
285
- }
286
-
287
- // View logs
288
- function viewLogs(id, name) {
289
- logsAgentId = id
290
- document.getElementById('logs-title').textContent = `Logs: ${name}`
291
- document.getElementById('logs-modal').classList.add('active')
292
- loadLogs()
293
-
294
- // Auto-refresh logs every 2 seconds
295
- logsInterval = setInterval(() => {
296
- if (document.getElementById('logs-autoscroll').checked) {
297
- loadLogs()
298
- }
299
- }, 2000)
332
+ } catch (e) { alert(e.message) }
300
333
  }
301
334
 
302
- // Load logs
303
- async function loadLogs() {
304
- if (!logsAgentId) return
335
+ // ── Utilities ──────────────────────────────────────────────────────
305
336
 
306
- try {
307
- const res = await fetch(`${API_BASE}/agents/${logsAgentId}/logs?lines=200`)
308
- const data = await res.json()
309
-
310
- const logsContent = document.getElementById('logs-content')
311
- logsContent.textContent = data.logs.join('\n')
312
-
313
- // Auto-scroll to bottom if enabled
314
- if (document.getElementById('logs-autoscroll').checked) {
315
- logsContent.scrollTop = logsContent.scrollHeight
316
- }
317
- } catch (err) {
318
- console.error('Failed to load logs:', err)
319
- }
337
+ function esc(text) {
338
+ const d = document.createElement('div')
339
+ d.textContent = text || ''
340
+ return d.innerHTML
320
341
  }
321
342
 
322
- // Clear logs
323
- async function clearLogs() {
324
- if (!logsAgentId) return
325
-
326
- if (!confirm('Are you sure you want to clear all logs for this agent?')) {
327
- return
328
- }
329
-
330
- try {
331
- const res = await fetch(`${API_BASE}/agents/${logsAgentId}/logs`, { method: 'DELETE' })
332
- const data = await res.json()
333
-
334
- if (!res.ok) {
335
- alert(`Failed to clear logs: ${data.error}`)
336
- return
337
- }
338
-
339
- loadLogs()
340
- } catch (err) {
341
- alert(`Failed to clear logs: ${err.message}`)
342
- }
343
+ function fmtUptime(ms) {
344
+ const s = Math.floor(ms / 1000)
345
+ const m = Math.floor(s / 60)
346
+ const h = Math.floor(m / 60)
347
+ const d = Math.floor(h / 24)
348
+ if (d > 0) return `${d}d ${h % 24}h`
349
+ if (h > 0) return `${h}h ${m % 60}m`
350
+ if (m > 0) return `${m}m ${s % 60}s`
351
+ return `${s}s`
343
352
  }
344
353
 
345
- // Close logs modal
346
- function closeLogsModal() {
347
- document.getElementById('logs-modal').classList.remove('active')
348
- if (logsInterval) {
349
- clearInterval(logsInterval)
350
- logsInterval = null
351
- }
352
- logsAgentId = null
354
+ function fmtBytes(bytes) {
355
+ if (bytes === 0) return '0 B'
356
+ const k = 1024
357
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
358
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
359
+ return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
353
360
  }
354
361
 
355
- // Utility: Escape HTML
356
- function escapeHtml(text) {
357
- const div = document.createElement('div')
358
- div.textContent = text
359
- return div.innerHTML
360
- }
362
+ // ── Init ───────────────────────────────────────────────────────────
361
363
 
362
- // Utility: Format uptime
363
- function formatUptime(ms) {
364
- const seconds = Math.floor(ms / 1000)
365
- const minutes = Math.floor(seconds / 60)
366
- const hours = Math.floor(minutes / 60)
367
- const days = Math.floor(hours / 24)
368
-
369
- if (days > 0) return `${days}d ${hours % 24}h`
370
- if (hours > 0) return `${hours}h ${minutes % 60}m`
371
- if (minutes > 0) return `${minutes}m ${seconds % 60}s`
372
- return `${seconds}s`
373
- }
374
-
375
- // Event listeners
376
364
  document.addEventListener('DOMContentLoaded', () => {
377
365
  loadSystemInfo()
366
+ loadHealth()
378
367
  loadAgents()
379
368
 
380
- // Refresh button
381
369
  document.getElementById('refresh-btn').addEventListener('click', () => {
382
- document.getElementById('refresh-btn').querySelector('.icon').classList.add('loading')
383
- Promise.all([loadSystemInfo(), loadAgents()]).then(() => {
384
- document.getElementById('refresh-btn').querySelector('.icon').classList.remove('loading')
370
+ const icon = document.getElementById('refresh-icon')
371
+ icon.classList.add('spinning')
372
+ Promise.all([loadSystemInfo(), loadHealth(), loadAgents()]).then(() => {
373
+ setTimeout(() => icon.classList.remove('spinning'), 500)
385
374
  })
386
375
  })
387
376
 
388
- // Add agent button
389
377
  document.getElementById('add-agent-btn').addEventListener('click', openAddAgentModal)
390
-
391
- // Agent form submit
392
378
  document.getElementById('agent-form').addEventListener('submit', saveAgent)
393
379
 
394
- // Logs controls
395
- document.getElementById('logs-refresh').addEventListener('click', loadLogs)
396
- document.getElementById('logs-clear').addEventListener('click', clearLogs)
397
-
398
- // Auto-refresh agents every 5 seconds
380
+ // Auto-refresh
399
381
  setInterval(loadAgents, 5000)
382
+ setInterval(loadHealth, 10000)
400
383
  })
401
384
 
402
385
  // Close modals on background click
403
386
  window.addEventListener('click', (e) => {
404
387
  if (e.target.classList.contains('modal')) {
405
388
  e.target.classList.remove('active')
406
- if (e.target.id === 'logs-modal' && logsInterval) {
407
- clearInterval(logsInterval)
408
- logsInterval = null
409
- }
410
389
  }
411
390
  })