orquesta-agent 0.2.53 → 0.2.55

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