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.
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +8 -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 +59 -4
- package/dist/ui/manager.js.map +1 -1
- package/dist/ui/public/app.js +247 -306
- package/dist/ui/public/favicon.ico +0 -0
- package/dist/ui/public/index.html +129 -107
- package/dist/ui/public/logo-mark.png +0 -0
- package/dist/ui/public/logo.svg +16 -0
- package/dist/ui/public/style.css +393 -311
- package/dist/ui/server.d.ts.map +1 -1
- package/dist/ui/server.js +61 -0
- package/dist/ui/server.js.map +1 -1
- package/package.json +7 -3
- package/scripts/build-binary.sh +60 -0
- package/scripts/generate-manifest.sh +71 -0
- package/scripts/postinstall.js +3 -0
package/dist/ui/public/app.js
CHANGED
|
@@ -1,411 +1,352 @@
|
|
|
1
|
-
// Orquesta Agent Manager
|
|
1
|
+
// Orquesta Agent Manager
|
|
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 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(`${
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
if (systemInfo.hasClaudeCli) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
53
|
+
cd.className = 'cli-dot'; cs.textContent = 'missing'
|
|
30
54
|
}
|
|
31
55
|
|
|
32
|
-
// Orquesta
|
|
33
|
-
const
|
|
56
|
+
// Orquesta
|
|
57
|
+
const od = document.getElementById('orquesta-dot')
|
|
58
|
+
const os2 = document.getElementById('orquesta-status')
|
|
34
59
|
if (systemInfo.hasOrquestaCli) {
|
|
35
|
-
|
|
60
|
+
od.className = 'cli-dot on'; os2.textContent = 'ready'
|
|
36
61
|
} else {
|
|
37
|
-
|
|
62
|
+
od.className = 'cli-dot'; os2.textContent = 'missing'
|
|
38
63
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
64
|
+
|
|
65
|
+
document.getElementById('node-status').textContent = systemInfo.nodeVersion
|
|
66
|
+
} catch {}
|
|
42
67
|
}
|
|
43
68
|
|
|
44
|
-
//
|
|
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(`${
|
|
109
|
+
const res = await fetch(`${API}/agents`)
|
|
48
110
|
agents = await res.json()
|
|
49
111
|
renderAgents()
|
|
50
|
-
} catch
|
|
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
|
|
58
|
-
const
|
|
116
|
+
const c = document.getElementById('agents-container')
|
|
117
|
+
const countEl = document.getElementById('agent-count')
|
|
59
118
|
|
|
60
119
|
if (agents.length === 0) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
<div class="
|
|
99
|
-
<
|
|
100
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
//
|
|
185
|
+
// ── Agent Actions ──────────────────────────────────────────────────
|
|
186
|
+
|
|
139
187
|
async function startAgent(id) {
|
|
140
188
|
try {
|
|
141
|
-
const res = await fetch(`${
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
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(`${
|
|
159
|
-
const
|
|
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 (
|
|
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(`${
|
|
176
|
-
const
|
|
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 (
|
|
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(`
|
|
192
|
-
return
|
|
193
|
-
}
|
|
194
|
-
|
|
212
|
+
if (!confirm(`Delete agent "${name}"?`)) return
|
|
195
213
|
try {
|
|
196
|
-
const res = await fetch(`${
|
|
197
|
-
const
|
|
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
|
-
|
|
200
|
-
alert(`Failed to delete agent: ${data.error}`)
|
|
201
|
-
return
|
|
202
|
-
}
|
|
221
|
+
// ── Log Panel ──────────────────────────────────────────────────────
|
|
203
222
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
221
|
-
if (!
|
|
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 =
|
|
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-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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:
|
|
306
|
+
autoStart: false,
|
|
252
307
|
autoPull: document.getElementById('agent-auto-pull').checked,
|
|
253
308
|
}
|
|
254
|
-
|
|
255
309
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
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
|
-
//
|
|
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
|