orquesta-agent 0.2.57 → 0.2.59
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/executor.d.ts.map +1 -1
- package/dist/executor.js +6 -0
- package/dist/executor.js.map +1 -1
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/ui/manager.d.ts.map +1 -1
- package/dist/ui/manager.js +13 -3
- package/dist/ui/manager.js.map +1 -1
- package/dist/ui/public/app.js +161 -203
- package/dist/ui/public/index.html +125 -139
- package/dist/ui/public/style.css +339 -391
- package/dist/ui/server.d.ts.map +1 -1
- package/dist/ui/server.js +22 -0
- package/dist/ui/server.js.map +1 -1
- package/package.json +10 -3
- package/scripts/build-binary.sh +60 -0
- package/scripts/generate-manifest.sh +71 -0
package/dist/ui/public/app.js
CHANGED
|
@@ -1,27 +1,72 @@
|
|
|
1
|
-
// Orquesta Agent Manager
|
|
1
|
+
// Orquesta Agent Manager
|
|
2
2
|
|
|
3
3
|
const API = '/api'
|
|
4
4
|
let agents = []
|
|
5
5
|
let systemInfo = null
|
|
6
6
|
let editingAgentId = null
|
|
7
|
-
let
|
|
8
|
-
let
|
|
7
|
+
let openAgentId = null
|
|
8
|
+
let openPanel = 'logs'
|
|
9
|
+
let logSources = new Map() // agentId → EventSource
|
|
9
10
|
|
|
10
|
-
// ── Icons
|
|
11
|
+
// ── SVG Icons ──────────────────────────────────────────────────────
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
-
|
|
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>',
|
|
14
16
|
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"
|
|
16
|
-
|
|
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>',
|
|
17
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>',
|
|
18
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>',
|
|
19
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>',
|
|
20
|
-
|
|
21
|
-
|
|
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>',
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
// ──
|
|
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 ────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
async function loadSystemInfo() {
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(`${API}/system`)
|
|
42
|
+
systemInfo = await res.json()
|
|
43
|
+
document.getElementById('version').textContent = `v${systemInfo.version}`
|
|
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'
|
|
52
|
+
} else {
|
|
53
|
+
cd.className = 'cli-dot'; cs.textContent = 'missing'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Orquesta
|
|
57
|
+
const od = document.getElementById('orquesta-dot')
|
|
58
|
+
const os2 = document.getElementById('orquesta-status')
|
|
59
|
+
if (systemInfo.hasOrquestaCli) {
|
|
60
|
+
od.className = 'cli-dot on'; os2.textContent = 'ready'
|
|
61
|
+
} else {
|
|
62
|
+
od.className = 'cli-dot'; os2.textContent = 'missing'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
document.getElementById('node-status').textContent = systemInfo.nodeVersion
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Health ─────────────────────────────────────────────────────────
|
|
25
70
|
|
|
26
71
|
async function loadHealth() {
|
|
27
72
|
try {
|
|
@@ -29,82 +74,32 @@ async function loadHealth() {
|
|
|
29
74
|
if (!res.ok) return
|
|
30
75
|
const h = await res.json()
|
|
31
76
|
|
|
32
|
-
// CPU
|
|
33
77
|
const cpuPct = h.cpu.usage
|
|
34
|
-
document.getElementById('cpu-value').
|
|
78
|
+
document.getElementById('cpu-value').textContent = cpuPct + '%'
|
|
35
79
|
setBar('cpu-bar', cpuPct)
|
|
36
80
|
document.getElementById('cpu-detail').textContent = `${h.cpu.count} cores`
|
|
37
81
|
|
|
38
|
-
// Memory
|
|
39
82
|
const memPct = Math.round((h.memory.used / h.memory.total) * 100)
|
|
40
|
-
document.getElementById('mem-value').
|
|
83
|
+
document.getElementById('mem-value').textContent = memPct + '%'
|
|
41
84
|
setBar('mem-bar', memPct)
|
|
42
85
|
document.getElementById('mem-detail').textContent = `${fmtBytes(h.memory.used)} / ${fmtBytes(h.memory.total)}`
|
|
43
86
|
|
|
44
|
-
// Disk
|
|
45
87
|
if (h.disk.total > 0) {
|
|
46
88
|
const diskPct = Math.round((h.disk.used / h.disk.total) * 100)
|
|
47
|
-
document.getElementById('disk-value').
|
|
89
|
+
document.getElementById('disk-value').textContent = diskPct + '%'
|
|
48
90
|
setBar('disk-bar', diskPct)
|
|
49
|
-
document.getElementById('disk-detail').textContent = `${fmtBytes(h.disk.free)} free
|
|
50
|
-
} else {
|
|
51
|
-
document.getElementById('disk-value').innerHTML = `N/A`
|
|
91
|
+
document.getElementById('disk-detail').textContent = `${fmtBytes(h.disk.free)} free`
|
|
52
92
|
}
|
|
53
93
|
|
|
54
|
-
// Uptime
|
|
55
94
|
document.getElementById('uptime-value').textContent = fmtUptime(h.uptime * 1000)
|
|
56
95
|
document.getElementById('hostname-detail').textContent = h.hostname
|
|
57
|
-
} catch {
|
|
96
|
+
} catch {}
|
|
58
97
|
}
|
|
59
98
|
|
|
60
99
|
function setBar(id, pct) {
|
|
61
100
|
const el = document.getElementById(id)
|
|
62
|
-
el.style.width =
|
|
63
|
-
el.className = 'health-bar-fill
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ── System Info ────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
async function loadSystemInfo() {
|
|
69
|
-
try {
|
|
70
|
-
const res = await fetch(`${API}/system`)
|
|
71
|
-
systemInfo = await res.json()
|
|
72
|
-
|
|
73
|
-
document.getElementById('version').textContent = `v${systemInfo.version}`
|
|
74
|
-
|
|
75
|
-
// Claude CLI mode
|
|
76
|
-
const claudeCard = document.getElementById('mode-claude')
|
|
77
|
-
const claudeDot = document.getElementById('claude-dot')
|
|
78
|
-
const claudeLabel = document.getElementById('claude-label')
|
|
79
|
-
if (systemInfo.hasClaudeCli && systemInfo.claudeAuth?.authenticated) {
|
|
80
|
-
claudeDot.className = 'dot on'
|
|
81
|
-
claudeLabel.textContent = `Ready (${systemInfo.claudeAuth.method})`
|
|
82
|
-
claudeCard.classList.add('active')
|
|
83
|
-
} else if (systemInfo.hasClaudeCli) {
|
|
84
|
-
claudeDot.className = 'dot warn'
|
|
85
|
-
claudeLabel.textContent = 'Installed but not authenticated'
|
|
86
|
-
} else {
|
|
87
|
-
claudeDot.className = 'dot off'
|
|
88
|
-
claudeLabel.textContent = 'Not installed'
|
|
89
|
-
}
|
|
90
|
-
|
|
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')
|
|
95
|
-
if (systemInfo.hasOrquestaCli) {
|
|
96
|
-
orqDot.className = 'dot on'
|
|
97
|
-
orqLabel.textContent = 'Ready (Claude, GPT-4o, DeepSeek, Gemini...)'
|
|
98
|
-
orqCard.classList.add('active')
|
|
99
|
-
} else {
|
|
100
|
-
orqDot.className = 'dot off'
|
|
101
|
-
orqLabel.textContent = 'Not installed — npm i -g orquesta-cli'
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Node
|
|
105
|
-
document.getElementById('node-dot').className = 'dot on'
|
|
106
|
-
document.getElementById('node-label').textContent = systemInfo.nodeVersion
|
|
107
|
-
} catch { /* ignore */ }
|
|
101
|
+
el.style.width = Math.min(pct, 100) + '%'
|
|
102
|
+
el.className = 'health-bar-fill' + (pct > 90 ? ' red' : pct > 70 ? ' yellow' : '')
|
|
108
103
|
}
|
|
109
104
|
|
|
110
105
|
// ── Agents ─────────────────────────────────────────────────────────
|
|
@@ -114,79 +109,77 @@ async function loadAgents() {
|
|
|
114
109
|
const res = await fetch(`${API}/agents`)
|
|
115
110
|
agents = await res.json()
|
|
116
111
|
renderAgents()
|
|
117
|
-
} catch {
|
|
112
|
+
} catch {}
|
|
118
113
|
}
|
|
119
114
|
|
|
120
115
|
function renderAgents() {
|
|
121
|
-
const
|
|
122
|
-
const emptyState = document.getElementById('empty-state')
|
|
116
|
+
const c = document.getElementById('agents-container')
|
|
123
117
|
const countEl = document.getElementById('agent-count')
|
|
124
118
|
|
|
125
119
|
if (agents.length === 0) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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>`
|
|
130
127
|
return
|
|
131
128
|
}
|
|
132
129
|
|
|
133
|
-
emptyState.style.display = 'none'
|
|
134
130
|
const running = agents.filter(a => a.status === 'running').length
|
|
135
|
-
countEl.textContent =
|
|
131
|
+
countEl.textContent = `${running} of ${agents.length} online`
|
|
136
132
|
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
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}">
|
|
141
139
|
<div class="agent-card-header">
|
|
142
|
-
<div class="agent-
|
|
143
|
-
|
|
144
|
-
<div>
|
|
145
|
-
<
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
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>` : ''}
|
|
149
150
|
</div>
|
|
150
151
|
</div>
|
|
151
|
-
<div class="agent-
|
|
152
|
+
<div class="agent-actions">
|
|
152
153
|
${a.status === 'running' ? `
|
|
153
|
-
<button class="btn btn-sm" onclick="stopAgent('${a.id}')" title="Stop">${
|
|
154
|
-
<button class="btn btn-sm" onclick="restartAgent('${a.id}')" title="Restart">${
|
|
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>
|
|
155
156
|
` : `
|
|
156
|
-
<button class="btn btn-sm btn-
|
|
157
|
+
<button class="btn btn-sm btn-primary" onclick="startAgent('${a.id}')" title="Start">${I.play}</button>
|
|
157
158
|
`}
|
|
158
|
-
<button class="btn btn-sm" onclick="
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
` : ''}
|
|
161
164
|
</div>
|
|
162
165
|
</div>
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
${a.uptime ? `<div class="meta-item"><span class="meta-label">Uptime</span><span class="meta-value">${fmtUptime(a.uptime)}</span></div>` : ''}
|
|
170
|
-
${a.error ? `<div class="meta-item"><span class="meta-label" style="color:var(--red)">Error</span><span class="meta-value" style="color:var(--red)">${esc(a.error)}</span></div>` : ''}
|
|
171
|
-
</div>
|
|
172
|
-
|
|
173
|
-
<div class="agent-logs ${isLogsOpen ? 'open' : ''}" id="logs-${a.id}">
|
|
174
|
-
<div class="agent-logs-header">
|
|
175
|
-
<span>Live Logs</span>
|
|
176
|
-
<div style="display:flex;gap:6px">
|
|
177
|
-
<button class="btn btn-sm" onclick="refreshLogs('${a.id}')">Refresh</button>
|
|
178
|
-
<button class="btn btn-sm btn-danger" onclick="clearLogs('${a.id}')">Clear</button>
|
|
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>
|
|
179
172
|
</div>
|
|
180
173
|
</div>
|
|
181
|
-
<
|
|
174
|
+
<div class="agent-panel active" id="panel-logs-${a.id}">
|
|
175
|
+
<div class="log-viewer" id="logs-${a.id}"></div>
|
|
176
|
+
</div>
|
|
182
177
|
</div>
|
|
183
178
|
</div>`
|
|
184
|
-
}).join('')
|
|
179
|
+
}).join('') + '</div>'
|
|
185
180
|
|
|
186
|
-
//
|
|
187
|
-
if (
|
|
188
|
-
refreshLogs(openLogsId)
|
|
189
|
-
}
|
|
181
|
+
// Reconnect SSE for open panel
|
|
182
|
+
if (openAgentId) connectLogStream(openAgentId)
|
|
190
183
|
}
|
|
191
184
|
|
|
192
185
|
// ── Agent Actions ──────────────────────────────────────────────────
|
|
@@ -195,7 +188,7 @@ async function startAgent(id) {
|
|
|
195
188
|
try {
|
|
196
189
|
const res = await fetch(`${API}/agents/${id}/start`, { method: 'POST' })
|
|
197
190
|
if (!res.ok) { const d = await res.json(); alert(d.error); return }
|
|
198
|
-
setTimeout(loadAgents,
|
|
191
|
+
setTimeout(loadAgents, 1500)
|
|
199
192
|
} catch (e) { alert(e.message) }
|
|
200
193
|
}
|
|
201
194
|
|
|
@@ -216,72 +209,61 @@ async function restartAgent(id) {
|
|
|
216
209
|
}
|
|
217
210
|
|
|
218
211
|
async function deleteAgent(id, name) {
|
|
219
|
-
if (!confirm(`Delete agent "${name}"
|
|
212
|
+
if (!confirm(`Delete agent "${name}"?`)) return
|
|
220
213
|
try {
|
|
221
214
|
const res = await fetch(`${API}/agents/${id}`, { method: 'DELETE' })
|
|
222
215
|
if (!res.ok) { const d = await res.json(); alert(d.error); return }
|
|
223
|
-
if (
|
|
216
|
+
if (openAgentId === id) { disconnectLogStream(id); openAgentId = null }
|
|
224
217
|
loadAgents()
|
|
225
218
|
} catch (e) { alert(e.message) }
|
|
226
219
|
}
|
|
227
220
|
|
|
228
|
-
// ──
|
|
221
|
+
// ── Log Panel ──────────────────────────────────────────────────────
|
|
229
222
|
|
|
230
|
-
function
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (el) el.classList.remove('open')
|
|
235
|
-
stopLogsPolling(id)
|
|
223
|
+
function togglePanel(id) {
|
|
224
|
+
if (openAgentId === id) {
|
|
225
|
+
disconnectLogStream(id)
|
|
226
|
+
openAgentId = null
|
|
236
227
|
} else {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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)
|
|
228
|
+
if (openAgentId) disconnectLogStream(openAgentId)
|
|
229
|
+
openAgentId = id
|
|
230
|
+
connectLogStream(id)
|
|
248
231
|
}
|
|
249
|
-
// Re-render buttons (toggle icon)
|
|
250
232
|
renderAgents()
|
|
251
233
|
}
|
|
252
234
|
|
|
253
|
-
function
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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)
|
|
264
254
|
}
|
|
255
|
+
logSources.set(id, es)
|
|
265
256
|
}
|
|
266
257
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const data = await res.json()
|
|
271
|
-
const el = document.getElementById(`logs-content-${id}`)
|
|
272
|
-
if (el) {
|
|
273
|
-
el.textContent = data.logs.join('\n') || 'No logs yet'
|
|
274
|
-
el.scrollTop = el.scrollHeight
|
|
275
|
-
}
|
|
276
|
-
} catch { /* ignore */ }
|
|
258
|
+
function disconnectLogStream(id) {
|
|
259
|
+
const es = logSources.get(id)
|
|
260
|
+
if (es) { es.close(); logSources.delete(id) }
|
|
277
261
|
}
|
|
278
262
|
|
|
279
263
|
async function clearLogs(id) {
|
|
280
|
-
if (!confirm('Clear all logs for this agent?')) return
|
|
281
264
|
try {
|
|
282
265
|
await fetch(`${API}/agents/${id}/logs`, { method: 'DELETE' })
|
|
283
|
-
|
|
284
|
-
} catch { /* ignore */ }
|
|
266
|
+
} catch {}
|
|
285
267
|
}
|
|
286
268
|
|
|
287
269
|
// ── Modal ──────────────────────────────────────────────────────────
|
|
@@ -304,7 +286,6 @@ function editAgent(id) {
|
|
|
304
286
|
document.getElementById('agent-token').value = a.token
|
|
305
287
|
document.getElementById('agent-cli-preference').value = a.cliPreference
|
|
306
288
|
document.getElementById('agent-permission-mode').value = a.permissionMode
|
|
307
|
-
document.getElementById('agent-auto-start').checked = a.autoStart
|
|
308
289
|
document.getElementById('agent-auto-pull').checked = a.autoPull
|
|
309
290
|
document.getElementById('agent-modal').classList.add('active')
|
|
310
291
|
}
|
|
@@ -314,18 +295,17 @@ function closeAgentModal() {
|
|
|
314
295
|
editingAgentId = null
|
|
315
296
|
}
|
|
316
297
|
|
|
317
|
-
async function saveAgent(
|
|
318
|
-
|
|
298
|
+
async function saveAgent(e) {
|
|
299
|
+
e.preventDefault()
|
|
319
300
|
const body = {
|
|
320
301
|
name: document.getElementById('agent-name').value,
|
|
321
302
|
workingDir: document.getElementById('agent-working-dir').value,
|
|
322
303
|
token: document.getElementById('agent-token').value,
|
|
323
304
|
cliPreference: document.getElementById('agent-cli-preference').value,
|
|
324
305
|
permissionMode: document.getElementById('agent-permission-mode').value,
|
|
325
|
-
autoStart:
|
|
306
|
+
autoStart: false,
|
|
326
307
|
autoPull: document.getElementById('agent-auto-pull').checked,
|
|
327
308
|
}
|
|
328
|
-
|
|
329
309
|
try {
|
|
330
310
|
const url = editingAgentId ? `${API}/agents/${editingAgentId}` : `${API}/agents`
|
|
331
311
|
const method = editingAgentId ? 'PUT' : 'POST'
|
|
@@ -333,34 +313,25 @@ async function saveAgent(event) {
|
|
|
333
313
|
if (!res.ok) { const d = await res.json(); alert(d.error); return }
|
|
334
314
|
closeAgentModal()
|
|
335
315
|
loadAgents()
|
|
336
|
-
} catch (
|
|
316
|
+
} catch (e2) { alert(e2.message) }
|
|
337
317
|
}
|
|
338
318
|
|
|
339
319
|
// ── Utilities ──────────────────────────────────────────────────────
|
|
340
320
|
|
|
341
|
-
function esc(
|
|
342
|
-
const d = document.createElement('div')
|
|
343
|
-
d.textContent = text || ''
|
|
344
|
-
return d.innerHTML
|
|
345
|
-
}
|
|
321
|
+
function esc(t) { const d = document.createElement('div'); d.textContent = t || ''; return d.innerHTML }
|
|
346
322
|
|
|
347
323
|
function fmtUptime(ms) {
|
|
348
|
-
const s = Math.floor(ms / 1000)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (h > 0) return `${h}h ${m % 60}m`
|
|
354
|
-
if (m > 0) return `${m}m ${s % 60}s`
|
|
355
|
-
return `${s}s`
|
|
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'
|
|
356
329
|
}
|
|
357
330
|
|
|
358
|
-
function fmtBytes(
|
|
359
|
-
if (
|
|
360
|
-
const k = 1024
|
|
361
|
-
|
|
362
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
363
|
-
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
|
|
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]
|
|
364
335
|
}
|
|
365
336
|
|
|
366
337
|
// ── Init ───────────────────────────────────────────────────────────
|
|
@@ -369,26 +340,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
369
340
|
loadSystemInfo()
|
|
370
341
|
loadHealth()
|
|
371
342
|
loadAgents()
|
|
372
|
-
|
|
373
|
-
document.getElementById('refresh-btn').addEventListener('click', () => {
|
|
374
|
-
const icon = document.getElementById('refresh-icon')
|
|
375
|
-
icon.classList.add('spinning')
|
|
376
|
-
Promise.all([loadSystemInfo(), loadHealth(), loadAgents()]).then(() => {
|
|
377
|
-
setTimeout(() => icon.classList.remove('spinning'), 500)
|
|
378
|
-
})
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
document.getElementById('add-agent-btn').addEventListener('click', openAddAgentModal)
|
|
382
343
|
document.getElementById('agent-form').addEventListener('submit', saveAgent)
|
|
383
|
-
|
|
384
|
-
// Auto-refresh
|
|
385
344
|
setInterval(loadAgents, 5000)
|
|
386
|
-
setInterval(loadHealth,
|
|
345
|
+
setInterval(loadHealth, 15000)
|
|
387
346
|
})
|
|
388
347
|
|
|
389
|
-
// Close modals on background click
|
|
390
348
|
window.addEventListener('click', (e) => {
|
|
391
|
-
if (e.target.classList.contains('modal')) {
|
|
349
|
+
if (e.target.classList.contains('modal-overlay')) {
|
|
392
350
|
e.target.classList.remove('active')
|
|
393
351
|
}
|
|
394
352
|
})
|