orquesta-agent 0.2.58 → 0.2.60

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,411 +1,352 @@
1
- // Orquesta Agent Manager Frontend
1
+ // Orquesta Agent Manager
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 openAgentId = null
8
+ let openPanel = 'logs'
9
+ let logSources = new Map() // agentId → EventSource
10
+
11
+ // ── SVG Icons ──────────────────────────────────────────────────────
12
+
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>',
16
+ stop: '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="1"/></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>',
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>',
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>',
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>',
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>',
25
+ }
26
+
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 ────────────────────────────────────────────────────
9
38
 
10
- // Load system info
11
39
  async function loadSystemInfo() {
12
40
  try {
13
- const res = await fetch(`${API_BASE}/system`)
41
+ const res = await fetch(`${API}/system`)
14
42
  systemInfo = await res.json()
15
-
16
- // Update UI
17
43
  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
- }
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'
28
52
  } else {
29
- claudeStatus.innerHTML = `<span class="badge danger">✗ Not installed</span>`
53
+ cd.className = 'cli-dot'; cs.textContent = 'missing'
30
54
  }
31
55
 
32
- // Orquesta CLI status
33
- const orquestaStatus = document.getElementById('orquesta-status')
56
+ // Orquesta
57
+ const od = document.getElementById('orquesta-dot')
58
+ const os2 = document.getElementById('orquesta-status')
34
59
  if (systemInfo.hasOrquestaCli) {
35
- orquestaStatus.innerHTML = `<span class="badge success">✓ Installed</span>`
60
+ od.className = 'cli-dot on'; os2.textContent = 'ready'
36
61
  } else {
37
- orquestaStatus.innerHTML = `<span class="badge danger">✗ Not installed</span>`
62
+ od.className = 'cli-dot'; os2.textContent = 'missing'
38
63
  }
39
- } catch (err) {
40
- console.error('Failed to load system info:', err)
41
- }
64
+
65
+ document.getElementById('node-status').textContent = systemInfo.nodeVersion
66
+ } catch {}
42
67
  }
43
68
 
44
- // Load agents
69
+ // ── Health ─────────────────────────────────────────────────────────
70
+
71
+ async function loadHealth() {
72
+ try {
73
+ const res = await fetch(`${API}/system/health`)
74
+ if (!res.ok) return
75
+ const h = await res.json()
76
+
77
+ const cpuPct = h.cpu.usage
78
+ document.getElementById('cpu-value').textContent = cpuPct + '%'
79
+ setBar('cpu-bar', cpuPct)
80
+ document.getElementById('cpu-detail').textContent = `${h.cpu.count} cores`
81
+
82
+ const memPct = Math.round((h.memory.used / h.memory.total) * 100)
83
+ document.getElementById('mem-value').textContent = memPct + '%'
84
+ setBar('mem-bar', memPct)
85
+ document.getElementById('mem-detail').textContent = `${fmtBytes(h.memory.used)} / ${fmtBytes(h.memory.total)}`
86
+
87
+ if (h.disk.total > 0) {
88
+ const diskPct = Math.round((h.disk.used / h.disk.total) * 100)
89
+ document.getElementById('disk-value').textContent = diskPct + '%'
90
+ setBar('disk-bar', diskPct)
91
+ document.getElementById('disk-detail').textContent = `${fmtBytes(h.disk.free)} free`
92
+ }
93
+
94
+ document.getElementById('uptime-value').textContent = fmtUptime(h.uptime * 1000)
95
+ document.getElementById('hostname-detail').textContent = h.hostname
96
+ } catch {}
97
+ }
98
+
99
+ function setBar(id, pct) {
100
+ const el = document.getElementById(id)
101
+ el.style.width = Math.min(pct, 100) + '%'
102
+ el.className = 'health-bar-fill' + (pct > 90 ? ' red' : pct > 70 ? ' yellow' : '')
103
+ }
104
+
105
+ // ── Agents ─────────────────────────────────────────────────────────
106
+
45
107
  async function loadAgents() {
46
108
  try {
47
- const res = await fetch(`${API_BASE}/agents`)
109
+ const res = await fetch(`${API}/agents`)
48
110
  agents = await res.json()
49
111
  renderAgents()
50
- } catch (err) {
51
- console.error('Failed to load agents:', err)
52
- }
112
+ } catch {}
53
113
  }
54
114
 
55
- // Render agents list
56
115
  function renderAgents() {
57
- const container = document.getElementById('agents-container')
58
- const emptyState = document.getElementById('empty-state')
116
+ const c = document.getElementById('agents-container')
117
+ const countEl = document.getElementById('agent-count')
59
118
 
60
119
  if (agents.length === 0) {
61
- emptyState.style.display = 'block'
62
- container.innerHTML = ''
63
- container.appendChild(emptyState)
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>`
64
127
  return
65
128
  }
66
129
 
67
- 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>
74
- </div>
75
- <span class="badge ${agent.status === 'running' ? 'success' : agent.status === 'error' ? 'danger' : ''}">${agent.status.toUpperCase()}</span>
76
- </div>
77
-
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>
130
+ const running = agents.filter(a => a.status === 'running').length
131
+ countEl.textContent = `${running} of ${agents.length} online`
132
+
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}">
139
+ <div class="agent-card-header">
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>` : ''}
150
+ </div>
90
151
  </div>
91
- ${agent.pid ? `
92
- <div class="info-row">
93
- <span class="label">PID:</span>
94
- <span class="value">${agent.pid}</span>
152
+ <div class="agent-actions">
153
+ ${a.status === 'running' ? `
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>
156
+ ` : `
157
+ <button class="btn btn-sm btn-primary" onclick="startAgent('${a.id}')" title="Start">${I.play}</button>
158
+ `}
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
+ ` : ''}
95
164
  </div>
96
- ` : ''}
97
- ${agent.uptime ? `
98
- <div class="info-row">
99
- <span class="label">Uptime:</span>
100
- <span class="value">${formatUptime(agent.uptime)}</span>
165
+ </div>
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>
172
+ </div>
101
173
  </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>
174
+ <div class="agent-panel active" id="panel-logs-${a.id}">
175
+ <div class="log-viewer" id="logs-${a.id}"></div>
107
176
  </div>
108
- ` : ''}
109
177
  </div>
178
+ </div>`
179
+ }).join('') + '</div>'
110
180
 
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('')
181
+ // Reconnect SSE for open panel
182
+ if (openAgentId) connectLogStream(openAgentId)
136
183
  }
137
184
 
138
- // Start agent
185
+ // ── Agent Actions ──────────────────────────────────────────────────
186
+
139
187
  async function startAgent(id) {
140
188
  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
-
149
- setTimeout(loadAgents, 1000)
150
- } catch (err) {
151
- alert(`Failed to start agent: ${err.message}`)
152
- }
189
+ const res = await fetch(`${API}/agents/${id}/start`, { method: 'POST' })
190
+ if (!res.ok) { const d = await res.json(); alert(d.error); return }
191
+ setTimeout(loadAgents, 1500)
192
+ } catch (e) { alert(e.message) }
153
193
  }
154
194
 
155
- // Stop agent
156
195
  async function stopAgent(id) {
157
196
  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
-
197
+ const res = await fetch(`${API}/agents/${id}/stop`, { method: 'POST' })
198
+ if (!res.ok) { const d = await res.json(); alert(d.error); return }
166
199
  setTimeout(loadAgents, 1000)
167
- } catch (err) {
168
- alert(`Failed to stop agent: ${err.message}`)
169
- }
200
+ } catch (e) { alert(e.message) }
170
201
  }
171
202
 
172
- // Restart agent
173
203
  async function restartAgent(id) {
174
204
  try {
175
- const res = await fetch(`${API_BASE}/agents/${id}/restart`, { method: 'POST' })
176
- const data = await res.json()
177
-
178
- if (!res.ok) {
179
- alert(`Failed to restart agent: ${data.error}`)
180
- return
181
- }
182
-
205
+ const res = await fetch(`${API}/agents/${id}/restart`, { method: 'POST' })
206
+ if (!res.ok) { const d = await res.json(); alert(d.error); return }
183
207
  setTimeout(loadAgents, 2000)
184
- } catch (err) {
185
- alert(`Failed to restart agent: ${err.message}`)
186
- }
208
+ } catch (e) { alert(e.message) }
187
209
  }
188
210
 
189
- // Delete agent
190
211
  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
193
- }
194
-
212
+ if (!confirm(`Delete agent "${name}"?`)) return
195
213
  try {
196
- const res = await fetch(`${API_BASE}/agents/${id}`, { method: 'DELETE' })
197
- const data = await res.json()
214
+ const res = await fetch(`${API}/agents/${id}`, { method: 'DELETE' })
215
+ if (!res.ok) { const d = await res.json(); alert(d.error); return }
216
+ if (openAgentId === id) { disconnectLogStream(id); openAgentId = null }
217
+ loadAgents()
218
+ } catch (e) { alert(e.message) }
219
+ }
198
220
 
199
- if (!res.ok) {
200
- alert(`Failed to delete agent: ${data.error}`)
201
- return
202
- }
221
+ // ── Log Panel ──────────────────────────────────────────────────────
203
222
 
204
- loadAgents()
205
- } catch (err) {
206
- alert(`Failed to delete agent: ${err.message}`)
223
+ function togglePanel(id) {
224
+ if (openAgentId === id) {
225
+ disconnectLogStream(id)
226
+ openAgentId = null
227
+ } else {
228
+ if (openAgentId) disconnectLogStream(openAgentId)
229
+ openAgentId = id
230
+ connectLogStream(id)
207
231
  }
232
+ renderAgents()
208
233
  }
209
234
 
210
- // Open add agent modal
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)
254
+ }
255
+ logSources.set(id, es)
256
+ }
257
+
258
+ function disconnectLogStream(id) {
259
+ const es = logSources.get(id)
260
+ if (es) { es.close(); logSources.delete(id) }
261
+ }
262
+
263
+ async function clearLogs(id) {
264
+ try {
265
+ await fetch(`${API}/agents/${id}/logs`, { method: 'DELETE' })
266
+ } catch {}
267
+ }
268
+
269
+ // ── Modal ──────────────────────────────────────────────────────────
270
+
211
271
  function openAddAgentModal() {
212
272
  editingAgentId = null
213
- document.getElementById('modal-title').textContent = 'Add New Agent'
273
+ document.getElementById('modal-title').textContent = 'Add Agent'
214
274
  document.getElementById('agent-form').reset()
275
+ document.getElementById('agent-auto-pull').checked = true
215
276
  document.getElementById('agent-modal').classList.add('active')
216
277
  }
217
278
 
218
- // Open edit agent modal
219
279
  function editAgent(id) {
220
- const agent = agents.find(a => a.id === id)
221
- if (!agent) return
222
-
280
+ const a = agents.find(x => x.id === id)
281
+ if (!a) return
223
282
  editingAgentId = id
224
283
  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
284
+ document.getElementById('agent-name').value = a.name
285
+ document.getElementById('agent-working-dir').value = a.workingDir
286
+ document.getElementById('agent-token').value = a.token
287
+ document.getElementById('agent-cli-preference').value = a.cliPreference
288
+ document.getElementById('agent-permission-mode').value = a.permissionMode
289
+ document.getElementById('agent-auto-pull').checked = a.autoPull
232
290
  document.getElementById('agent-modal').classList.add('active')
233
291
  }
234
292
 
235
- // Close agent modal
236
293
  function closeAgentModal() {
237
294
  document.getElementById('agent-modal').classList.remove('active')
238
295
  editingAgentId = null
239
296
  }
240
297
 
241
- // Save agent
242
- async function saveAgent(event) {
243
- event.preventDefault()
244
-
245
- const formData = {
298
+ async function saveAgent(e) {
299
+ e.preventDefault()
300
+ const body = {
246
301
  name: document.getElementById('agent-name').value,
247
302
  workingDir: document.getElementById('agent-working-dir').value,
248
303
  token: document.getElementById('agent-token').value,
249
304
  cliPreference: document.getElementById('agent-cli-preference').value,
250
305
  permissionMode: document.getElementById('agent-permission-mode').value,
251
- autoStart: document.getElementById('agent-auto-start').checked,
306
+ autoStart: false,
252
307
  autoPull: document.getElementById('agent-auto-pull').checked,
253
308
  }
254
-
255
309
  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
-
310
+ const url = editingAgentId ? `${API}/agents/${editingAgentId}` : `${API}/agents`
311
+ const method = editingAgentId ? 'PUT' : 'POST'
312
+ const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
313
+ if (!res.ok) { const d = await res.json(); alert(d.error); return }
280
314
  closeAgentModal()
281
315
  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)
300
- }
301
-
302
- // Load logs
303
- async function loadLogs() {
304
- if (!logsAgentId) return
305
-
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
- }
316
+ } catch (e2) { alert(e2.message) }
320
317
  }
321
318
 
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
- }
319
+ // ── Utilities ──────────────────────────────────────────────────────
338
320
 
339
- loadLogs()
340
- } catch (err) {
341
- alert(`Failed to clear logs: ${err.message}`)
342
- }
343
- }
321
+ function esc(t) { const d = document.createElement('div'); d.textContent = t || ''; return d.innerHTML }
344
322
 
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
323
+ function fmtUptime(ms) {
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'
353
329
  }
354
330
 
355
- // Utility: Escape HTML
356
- function escapeHtml(text) {
357
- const div = document.createElement('div')
358
- div.textContent = text
359
- return div.innerHTML
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]
360
335
  }
361
336
 
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
- }
337
+ // ── Init ───────────────────────────────────────────────────────────
374
338
 
375
- // Event listeners
376
339
  document.addEventListener('DOMContentLoaded', () => {
377
340
  loadSystemInfo()
341
+ loadHealth()
378
342
  loadAgents()
379
-
380
- // Refresh button
381
- 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')
385
- })
386
- })
387
-
388
- // Add agent button
389
- document.getElementById('add-agent-btn').addEventListener('click', openAddAgentModal)
390
-
391
- // Agent form submit
392
343
  document.getElementById('agent-form').addEventListener('submit', saveAgent)
393
-
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
399
344
  setInterval(loadAgents, 5000)
345
+ setInterval(loadHealth, 15000)
400
346
  })
401
347
 
402
- // Close modals on background click
403
348
  window.addEventListener('click', (e) => {
404
- if (e.target.classList.contains('modal')) {
349
+ if (e.target.classList.contains('modal-overlay')) {
405
350
  e.target.classList.remove('active')
406
- if (e.target.id === 'logs-modal' && logsInterval) {
407
- clearInterval(logsInterval)
408
- logsInterval = null
409
- }
410
351
  }
411
352
  })
Binary file