orquesta-agent 0.2.56 → 0.2.58

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,322 +1,248 @@
1
- // Orquesta Agent Manager Frontend
1
+ // Orquesta Agent Manager Frontend
2
2
 
3
- const API = '/api'
3
+ const API_BASE = '/api'
4
4
  let agents = []
5
5
  let systemInfo = null
6
6
  let editingAgentId = 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 ────────────────────────────────────────────────────
7
+ let logsAgentId = null
8
+ let logsInterval = null
67
9
 
10
+ // Load system info
68
11
  async function loadSystemInfo() {
69
12
  try {
70
- const res = await fetch(`${API}/system`)
13
+ const res = await fetch(`${API_BASE}/system`)
71
14
  systemInfo = await res.json()
72
15
 
16
+ // Update UI
73
17
  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'
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
+ }
86
28
  } else {
87
- claudeDot.className = 'dot off'
88
- claudeLabel.textContent = 'Not installed'
29
+ claudeStatus.innerHTML = `<span class="badge danger">✗ Not installed</span>`
89
30
  }
90
31
 
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')
32
+ // Orquesta CLI status
33
+ const orquestaStatus = document.getElementById('orquesta-status')
95
34
  if (systemInfo.hasOrquestaCli) {
96
- orqDot.className = 'dot on'
97
- orqLabel.textContent = 'Ready (Claude, GPT-4o, DeepSeek, Gemini...)'
98
- orqCard.classList.add('active')
35
+ orquestaStatus.innerHTML = `<span class="badge success">✓ Installed</span>`
99
36
  } else {
100
- orqDot.className = 'dot off'
101
- orqLabel.textContent = 'Not installed — npm i -g orquesta-cli'
37
+ orquestaStatus.innerHTML = `<span class="badge danger">✗ Not installed</span>`
102
38
  }
103
-
104
- // Node
105
- document.getElementById('node-dot').className = 'dot on'
106
- document.getElementById('node-label').textContent = systemInfo.nodeVersion
107
- } catch { /* ignore */ }
39
+ } catch (err) {
40
+ console.error('Failed to load system info:', err)
41
+ }
108
42
  }
109
43
 
110
- // ── Agents ─────────────────────────────────────────────────────────
111
-
44
+ // Load agents
112
45
  async function loadAgents() {
113
46
  try {
114
- const res = await fetch(`${API}/agents`)
47
+ const res = await fetch(`${API_BASE}/agents`)
115
48
  agents = await res.json()
116
49
  renderAgents()
117
- } catch { /* ignore */ }
50
+ } catch (err) {
51
+ console.error('Failed to load agents:', err)
52
+ }
118
53
  }
119
54
 
55
+ // Render agents list
120
56
  function renderAgents() {
121
57
  const container = document.getElementById('agents-container')
122
58
  const emptyState = document.getElementById('empty-state')
123
- const countEl = document.getElementById('agent-count')
124
59
 
125
60
  if (agents.length === 0) {
126
- emptyState.style.display = ''
61
+ emptyState.style.display = 'block'
127
62
  container.innerHTML = ''
128
63
  container.appendChild(emptyState)
129
- countEl.textContent = ''
130
64
  return
131
65
  }
132
66
 
133
67
  emptyState.style.display = 'none'
134
- const running = agents.filter(a => a.status === 'running').length
135
- countEl.textContent = `(${running}/${agents.length} running)`
136
-
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}">
141
- <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>
149
- </div>
150
- </div>
151
- <div class="agent-card-right">
152
- ${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>
155
- ` : `
156
- <button class="btn btn-sm btn-success" onclick="startAgent('${a.id}')" title="Start">${ICONS.play} <span class="btn-label">Start</span></button>
157
- `}
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>
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>
161
74
  </div>
75
+ <span class="badge ${agent.status === 'running' ? 'success' : agent.status === 'error' ? 'danger' : ''}">${agent.status.toUpperCase()}</span>
162
76
  </div>
163
77
 
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>
179
- </div>
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>
180
107
  </div>
181
- <pre class="logs-content" id="logs-content-${a.id}"></pre>
108
+ ` : ''}
182
109
  </div>
183
- </div>`
184
- }).join('')
185
110
 
186
- // Restore open logs
187
- if (openLogsId) {
188
- refreshLogs(openLogsId)
189
- }
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>
133
+ </div>
134
+ </div>
135
+ `).join('')
190
136
  }
191
137
 
192
- // ── Agent Actions ──────────────────────────────────────────────────
193
-
138
+ // Start agent
194
139
  async function startAgent(id) {
195
140
  try {
196
- const res = await fetch(`${API}/agents/${id}/start`, { method: 'POST' })
197
- if (!res.ok) { const d = await res.json(); alert(d.error); return }
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
+
198
149
  setTimeout(loadAgents, 1000)
199
- } catch (e) { alert(e.message) }
150
+ } catch (err) {
151
+ alert(`Failed to start agent: ${err.message}`)
152
+ }
200
153
  }
201
154
 
155
+ // Stop agent
202
156
  async function stopAgent(id) {
203
157
  try {
204
- const res = await fetch(`${API}/agents/${id}/stop`, { method: 'POST' })
205
- if (!res.ok) { const d = await res.json(); alert(d.error); return }
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
+
206
166
  setTimeout(loadAgents, 1000)
207
- } catch (e) { alert(e.message) }
167
+ } catch (err) {
168
+ alert(`Failed to stop agent: ${err.message}`)
169
+ }
208
170
  }
209
171
 
172
+ // Restart agent
210
173
  async function restartAgent(id) {
211
174
  try {
212
- const res = await fetch(`${API}/agents/${id}/restart`, { method: 'POST' })
213
- if (!res.ok) { const d = await res.json(); alert(d.error); return }
214
- setTimeout(loadAgents, 2000)
215
- } catch (e) { alert(e.message) }
216
- }
217
-
218
- async function deleteAgent(id, name) {
219
- if (!confirm(`Delete agent "${name}"? This will stop it and remove all configuration.`)) return
220
- try {
221
- const res = await fetch(`${API}/agents/${id}`, { method: 'DELETE' })
222
- if (!res.ok) { const d = await res.json(); alert(d.error); return }
223
- if (openLogsId === id) openLogsId = null
224
- loadAgents()
225
- } catch (e) { alert(e.message) }
226
- }
175
+ const res = await fetch(`${API_BASE}/agents/${id}/restart`, { method: 'POST' })
176
+ const data = await res.json()
227
177
 
228
- // ── Inline Logs ────────────────────────────────────────────────────
229
-
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)
236
- } else {
237
- // Close previous
238
- if (openLogsId) {
239
- const prev = document.getElementById(`logs-${openLogsId}`)
240
- if (prev) prev.classList.remove('open')
241
- stopLogsPolling(openLogsId)
178
+ if (!res.ok) {
179
+ alert(`Failed to restart agent: ${data.error}`)
180
+ return
242
181
  }
243
- openLogsId = id
244
- const el = document.getElementById(`logs-${id}`)
245
- if (el) el.classList.add('open')
246
- refreshLogs(id)
247
- startLogsPolling(id)
248
- }
249
- // Re-render buttons (toggle icon)
250
- renderAgents()
251
- }
252
182
 
253
- function startLogsPolling(id) {
254
- stopLogsPolling(id)
255
- const interval = setInterval(() => refreshLogs(id), 2000)
256
- logsIntervals.set(id, interval)
183
+ setTimeout(loadAgents, 2000)
184
+ } catch (err) {
185
+ alert(`Failed to restart agent: ${err.message}`)
186
+ }
257
187
  }
258
188
 
259
- function stopLogsPolling(id) {
260
- const interval = logsIntervals.get(id)
261
- if (interval) {
262
- clearInterval(interval)
263
- logsIntervals.delete(id)
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
264
193
  }
265
- }
266
194
 
267
- async function refreshLogs(id) {
268
195
  try {
269
- const res = await fetch(`${API}/agents/${id}/logs?lines=200`)
196
+ const res = await fetch(`${API_BASE}/agents/${id}`, { method: 'DELETE' })
270
197
  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
198
+
199
+ if (!res.ok) {
200
+ alert(`Failed to delete agent: ${data.error}`)
201
+ return
275
202
  }
276
- } catch { /* ignore */ }
277
- }
278
203
 
279
- async function clearLogs(id) {
280
- if (!confirm('Clear all logs for this agent?')) return
281
- try {
282
- await fetch(`${API}/agents/${id}/logs`, { method: 'DELETE' })
283
- refreshLogs(id)
284
- } catch { /* ignore */ }
204
+ loadAgents()
205
+ } catch (err) {
206
+ alert(`Failed to delete agent: ${err.message}`)
207
+ }
285
208
  }
286
209
 
287
- // ── Modal ──────────────────────────────────────────────────────────
288
-
210
+ // Open add agent modal
289
211
  function openAddAgentModal() {
290
212
  editingAgentId = null
291
- document.getElementById('modal-title').textContent = 'Add Agent'
213
+ document.getElementById('modal-title').textContent = 'Add New Agent'
292
214
  document.getElementById('agent-form').reset()
293
- document.getElementById('agent-auto-pull').checked = true
294
215
  document.getElementById('agent-modal').classList.add('active')
295
216
  }
296
217
 
218
+ // Open edit agent modal
297
219
  function editAgent(id) {
298
- const a = agents.find(x => x.id === id)
299
- if (!a) return
220
+ const agent = agents.find(a => a.id === id)
221
+ if (!agent) return
222
+
300
223
  editingAgentId = id
301
224
  document.getElementById('modal-title').textContent = 'Edit Agent'
302
- document.getElementById('agent-name').value = a.name
303
- document.getElementById('agent-working-dir').value = a.workingDir
304
- document.getElementById('agent-token').value = a.token
305
- document.getElementById('agent-cli-preference').value = a.cliPreference
306
- document.getElementById('agent-permission-mode').value = a.permissionMode
307
- document.getElementById('agent-auto-start').checked = a.autoStart
308
- document.getElementById('agent-auto-pull').checked = a.autoPull
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
309
232
  document.getElementById('agent-modal').classList.add('active')
310
233
  }
311
234
 
235
+ // Close agent modal
312
236
  function closeAgentModal() {
313
237
  document.getElementById('agent-modal').classList.remove('active')
314
238
  editingAgentId = null
315
239
  }
316
240
 
241
+ // Save agent
317
242
  async function saveAgent(event) {
318
243
  event.preventDefault()
319
- const body = {
244
+
245
+ const formData = {
320
246
  name: document.getElementById('agent-name').value,
321
247
  workingDir: document.getElementById('agent-working-dir').value,
322
248
  token: document.getElementById('agent-token').value,
@@ -327,68 +253,159 @@ async function saveAgent(event) {
327
253
  }
328
254
 
329
255
  try {
330
- const url = editingAgentId ? `${API}/agents/${editingAgentId}` : `${API}/agents`
331
- const method = editingAgentId ? 'PUT' : 'POST'
332
- const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
333
- if (!res.ok) { const d = await res.json(); alert(d.error); return }
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
+
334
280
  closeAgentModal()
335
281
  loadAgents()
336
- } catch (e) { alert(e.message) }
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)
337
300
  }
338
301
 
339
- // ── Utilities ──────────────────────────────────────────────────────
302
+ // Load logs
303
+ async function loadLogs() {
304
+ if (!logsAgentId) return
340
305
 
341
- function esc(text) {
342
- const d = document.createElement('div')
343
- d.textContent = text || ''
344
- return d.innerHTML
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
+ }
345
320
  }
346
321
 
347
- 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`
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
+ }
356
343
  }
357
344
 
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]}`
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
364
353
  }
365
354
 
366
- // ── Init ───────────────────────────────────────────────────────────
355
+ // Utility: Escape HTML
356
+ function escapeHtml(text) {
357
+ const div = document.createElement('div')
358
+ div.textContent = text
359
+ return div.innerHTML
360
+ }
367
361
 
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
368
376
  document.addEventListener('DOMContentLoaded', () => {
369
377
  loadSystemInfo()
370
- loadHealth()
371
378
  loadAgents()
372
379
 
380
+ // Refresh button
373
381
  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)
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')
378
385
  })
379
386
  })
380
387
 
388
+ // Add agent button
381
389
  document.getElementById('add-agent-btn').addEventListener('click', openAddAgentModal)
390
+
391
+ // Agent form submit
382
392
  document.getElementById('agent-form').addEventListener('submit', saveAgent)
383
393
 
384
- // Auto-refresh
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
385
399
  setInterval(loadAgents, 5000)
386
- setInterval(loadHealth, 10000)
387
400
  })
388
401
 
389
402
  // Close modals on background click
390
403
  window.addEventListener('click', (e) => {
391
404
  if (e.target.classList.contains('modal')) {
392
405
  e.target.classList.remove('active')
406
+ if (e.target.id === 'logs-modal' && logsInterval) {
407
+ clearInterval(logsInterval)
408
+ logsInterval = null
409
+ }
393
410
  }
394
411
  })