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.
- package/dist/ui/public/app.js +261 -282
- package/dist/ui/public/index.html +70 -63
- package/dist/ui/public/style.css +411 -260
- package/dist/ui/server.d.ts.map +1 -1
- package/dist/ui/server.js +39 -0
- package/dist/ui/server.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/public/app.js
CHANGED
|
@@ -1,248 +1,318 @@
|
|
|
1
|
-
// Orquesta Agent Manager Frontend
|
|
1
|
+
// Orquesta Agent Manager — Frontend
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const API = '/api'
|
|
4
4
|
let agents = []
|
|
5
5
|
let systemInfo = null
|
|
6
6
|
let editingAgentId = null
|
|
7
|
-
let
|
|
8
|
-
let
|
|
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(`${
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
if (systemInfo.hasClaudeCli) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
85
|
+
claudeDot.className = 'dot off'
|
|
86
|
+
claudeLabel.textContent = 'not installed'
|
|
30
87
|
}
|
|
31
88
|
|
|
32
|
-
// Orquesta CLI
|
|
33
|
-
const
|
|
89
|
+
// Orquesta CLI
|
|
90
|
+
const orqDot = document.getElementById('orquesta-dot')
|
|
91
|
+
const orqLabel = document.getElementById('orquesta-label')
|
|
34
92
|
if (systemInfo.hasOrquestaCli) {
|
|
35
|
-
|
|
93
|
+
orqDot.className = 'dot on'
|
|
94
|
+
orqLabel.textContent = 'installed'
|
|
36
95
|
} else {
|
|
37
|
-
|
|
96
|
+
orqDot.className = 'dot off'
|
|
97
|
+
orqLabel.textContent = 'not installed'
|
|
38
98
|
}
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
106
|
+
// ── Agents ─────────────────────────────────────────────────────────
|
|
107
|
+
|
|
45
108
|
async function loadAgents() {
|
|
46
109
|
try {
|
|
47
|
-
const res = await fetch(`${
|
|
110
|
+
const res = await fetch(`${API}/agents`)
|
|
48
111
|
agents = await res.json()
|
|
49
112
|
renderAgents()
|
|
50
|
-
} catch
|
|
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 = '
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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-
|
|
79
|
-
<div class="
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
</div
|
|
83
|
-
|
|
84
|
-
|
|
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-
|
|
112
|
-
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
179
|
+
</div>`
|
|
180
|
+
}).join('')
|
|
181
|
+
|
|
182
|
+
// Restore open logs
|
|
183
|
+
if (openLogsId) {
|
|
184
|
+
refreshLogs(openLogsId)
|
|
185
|
+
}
|
|
136
186
|
}
|
|
137
187
|
|
|
138
|
-
//
|
|
188
|
+
// ── Agent Actions ──────────────────────────────────────────────────
|
|
189
|
+
|
|
139
190
|
async function startAgent(id) {
|
|
140
191
|
try {
|
|
141
|
-
const res = await fetch(`${
|
|
142
|
-
const
|
|
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 (
|
|
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(`${
|
|
159
|
-
const
|
|
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 (
|
|
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(`${
|
|
176
|
-
const
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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(`${
|
|
265
|
+
const res = await fetch(`${API}/agents/${id}/logs?lines=200`)
|
|
197
266
|
const data = await res.json()
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
//
|
|
283
|
+
// ── Modal ──────────────────────────────────────────────────────────
|
|
284
|
+
|
|
211
285
|
function openAddAgentModal() {
|
|
212
286
|
editingAgentId = null
|
|
213
|
-
document.getElementById('modal-title').textContent = 'Add
|
|
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
|
|
221
|
-
if (!
|
|
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 =
|
|
226
|
-
document.getElementById('agent-working-dir').value =
|
|
227
|
-
document.getElementById('agent-token').value =
|
|
228
|
-
document.getElementById('agent-cli-preference').value =
|
|
229
|
-
document.getElementById('agent-permission-mode').value =
|
|
230
|
-
document.getElementById('agent-auto-start').checked =
|
|
231
|
-
document.getElementById('agent-auto-pull').checked =
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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 (
|
|
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
|
-
//
|
|
303
|
-
async function loadLogs() {
|
|
304
|
-
if (!logsAgentId) return
|
|
335
|
+
// ── Utilities ──────────────────────────────────────────────────────
|
|
305
336
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
//
|
|
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-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
//
|
|
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
|
})
|