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.
- package/dist/ui/public/app.js +265 -282
- package/dist/ui/public/index.html +99 -61
- package/dist/ui/public/style.css +458 -255
- 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,322 @@
|
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
87
|
+
claudeDot.className = 'dot off'
|
|
88
|
+
claudeLabel.textContent = 'Not installed'
|
|
30
89
|
}
|
|
31
90
|
|
|
32
|
-
// Orquesta CLI
|
|
33
|
-
const
|
|
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
|
-
|
|
96
|
+
orqDot.className = 'dot on'
|
|
97
|
+
orqLabel.textContent = 'Ready (Claude, GPT-4o, DeepSeek, Gemini...)'
|
|
98
|
+
orqCard.classList.add('active')
|
|
36
99
|
} else {
|
|
37
|
-
|
|
100
|
+
orqDot.className = 'dot off'
|
|
101
|
+
orqLabel.textContent = 'Not installed — npm i -g orquesta-cli'
|
|
38
102
|
}
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
110
|
+
// ── Agents ─────────────────────────────────────────────────────────
|
|
111
|
+
|
|
45
112
|
async function loadAgents() {
|
|
46
113
|
try {
|
|
47
|
-
const res = await fetch(`${
|
|
114
|
+
const res = await fetch(`${API}/agents`)
|
|
48
115
|
agents = await res.json()
|
|
49
116
|
renderAgents()
|
|
50
|
-
} catch
|
|
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 = '
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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-
|
|
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
|
-
` : ''}
|
|
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-
|
|
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>
|
|
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
|
-
|
|
183
|
+
</div>`
|
|
184
|
+
}).join('')
|
|
185
|
+
|
|
186
|
+
// Restore open logs
|
|
187
|
+
if (openLogsId) {
|
|
188
|
+
refreshLogs(openLogsId)
|
|
189
|
+
}
|
|
136
190
|
}
|
|
137
191
|
|
|
138
|
-
//
|
|
192
|
+
// ── Agent Actions ──────────────────────────────────────────────────
|
|
193
|
+
|
|
139
194
|
async function startAgent(id) {
|
|
140
195
|
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
|
-
|
|
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 (
|
|
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(`${
|
|
159
|
-
const
|
|
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 (
|
|
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(`${
|
|
176
|
-
const
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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(`${
|
|
269
|
+
const res = await fetch(`${API}/agents/${id}/logs?lines=200`)
|
|
197
270
|
const data = await res.json()
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
//
|
|
287
|
+
// ── Modal ──────────────────────────────────────────────────────────
|
|
288
|
+
|
|
211
289
|
function openAddAgentModal() {
|
|
212
290
|
editingAgentId = null
|
|
213
|
-
document.getElementById('modal-title').textContent = 'Add
|
|
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
|
|
221
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
303
|
-
async function loadLogs() {
|
|
304
|
-
if (!logsAgentId) return
|
|
339
|
+
// ── Utilities ──────────────────────────────────────────────────────
|
|
305
340
|
|
|
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
|
-
}
|
|
341
|
+
function esc(text) {
|
|
342
|
+
const d = document.createElement('div')
|
|
343
|
+
d.textContent = text || ''
|
|
344
|
+
return d.innerHTML
|
|
320
345
|
}
|
|
321
346
|
|
|
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
|
-
}
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
//
|
|
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-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
//
|
|
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
|
})
|