@agenticmail/enterprise 0.5.323 → 0.5.325
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/agent-heartbeat-BBINFNL4.js +510 -0
- package/dist/agent-heartbeat-UF2RKKS2.js +510 -0
- package/dist/chunk-4DBWU3P5.js +4929 -0
- package/dist/chunk-CQYLRIQ3.js +25938 -0
- package/dist/chunk-GYB2WHMN.js +5101 -0
- package/dist/chunk-KN3T3CTD.js +4929 -0
- package/dist/chunk-SVSLIQYN.js +1519 -0
- package/dist/chunk-VBTHTPZ6.js +26055 -0
- package/dist/chunk-WD72IOF2.js +5101 -0
- package/dist/chunk-ZGFDTW4H.js +1519 -0
- package/dist/cli-agent-USMKX7WN.js +2473 -0
- package/dist/cli-agent-ZIIFI77N.js +2473 -0
- package/dist/cli-serve-7JQ4FVUQ.js +260 -0
- package/dist/cli-serve-MLR4KAE2.js +260 -0
- package/dist/cli.js +3 -3
- package/dist/dashboard/app.js +4 -1
- package/dist/dashboard/components/icons.js +1 -0
- package/dist/dashboard/docs/cluster.html +285 -0
- package/dist/dashboard/pages/agent-detail/index.js +25 -3
- package/dist/dashboard/pages/agents.js +30 -1
- package/dist/dashboard/pages/cluster.js +512 -0
- package/dist/index.js +4 -4
- package/dist/routes-IGR6PZCA.js +92 -0
- package/dist/routes-XYR2RNEC.js +92 -0
- package/dist/runtime-EAWOE6JZ.js +45 -0
- package/dist/runtime-ZOC337DD.js +45 -0
- package/dist/server-7NT4LMSQ.js +28 -0
- package/dist/server-B3VJ6MSA.js +28 -0
- package/dist/setup-5YRQUOW2.js +20 -0
- package/dist/setup-6NUSB4XO.js +20 -0
- package/logs/cloudflared-error.log +8 -0
- package/logs/enterprise-out.log +3 -0
- package/package.json +1 -1
- package/src/cli-agent.ts +33 -1
- package/src/dashboard/app.js +4 -1
- package/src/dashboard/components/icons.js +1 -0
- package/src/dashboard/docs/cluster.html +285 -0
- package/src/dashboard/pages/agent-detail/index.js +25 -3
- package/src/dashboard/pages/agents.js +30 -1
- package/src/dashboard/pages/cluster.js +512 -0
- package/src/engine/cluster.ts +278 -0
- package/src/engine/routes.ts +163 -1
- package/src/engine/NOTE.MD +0 -52
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { h, useState, useEffect, useRef, Fragment, useApp, engineCall, apiCall } from '../components/utils.js';
|
|
2
|
+
import { I } from '../components/icons.js';
|
|
3
|
+
import { HelpButton } from '../components/settings-help.js';
|
|
4
|
+
import { KnowledgeLink } from '../components/knowledge-link.js';
|
|
5
|
+
|
|
6
|
+
function AddNodeModal({ onClose, onAdded, toast }) {
|
|
7
|
+
var [tab, setTab] = useState('manual'); // manual | ssh | script
|
|
8
|
+
var [form, setForm] = useState({ nodeId: '', name: '', host: '', port: 3101, sshHost: '', sshUser: 'root', sshKey: '', agentIds: '' });
|
|
9
|
+
var [testing, setTesting] = useState(false);
|
|
10
|
+
var [testResult, setTestResult] = useState(null);
|
|
11
|
+
var [saving, setSaving] = useState(false);
|
|
12
|
+
var [scriptGenerated, setScriptGenerated] = useState('');
|
|
13
|
+
|
|
14
|
+
var set = function(k, v) { setForm(function(f) { var n = Object.assign({}, f); n[k] = v; return n; }); };
|
|
15
|
+
|
|
16
|
+
var testConnection = function() {
|
|
17
|
+
if (!form.host) { toast('Host is required', 'error'); return; }
|
|
18
|
+
setTesting(true); setTestResult(null);
|
|
19
|
+
engineCall('/cluster/test-connection', {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
body: JSON.stringify({ host: form.host, port: form.port || 3101 }),
|
|
22
|
+
}).then(function(d) { setTestResult(d); setTesting(false); })
|
|
23
|
+
.catch(function(e) { setTestResult({ success: false, error: e.message }); setTesting(false); });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
var saveManual = function() {
|
|
27
|
+
if (!form.host) { toast('Host IP/hostname is required', 'error'); return; }
|
|
28
|
+
var nodeId = form.nodeId || form.host.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
29
|
+
setSaving(true);
|
|
30
|
+
engineCall('/cluster/register', {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
nodeId: nodeId,
|
|
34
|
+
name: form.name || nodeId,
|
|
35
|
+
host: form.host,
|
|
36
|
+
port: parseInt(form.port) || 3101,
|
|
37
|
+
}),
|
|
38
|
+
}).then(function(d) { toast('Node added successfully', 'success'); onAdded(); onClose(); })
|
|
39
|
+
.catch(function(e) { toast(e.message, 'error'); setSaving(false); });
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
var deploySsh = function() {
|
|
43
|
+
if (!form.sshHost) { toast('SSH host is required', 'error'); return; }
|
|
44
|
+
setSaving(true);
|
|
45
|
+
engineCall('/cluster/deploy-via-ssh', {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
host: form.sshHost,
|
|
49
|
+
user: form.sshUser || 'root',
|
|
50
|
+
privateKey: form.sshKey || undefined,
|
|
51
|
+
agentIds: form.agentIds ? form.agentIds.split(',').map(function(s) { return s.trim(); }) : [],
|
|
52
|
+
port: parseInt(form.port) || 3101,
|
|
53
|
+
name: form.name || form.sshHost,
|
|
54
|
+
}),
|
|
55
|
+
}).then(function(d) { toast('Deployment started: ' + (d.message || 'check progress'), 'success'); onAdded(); onClose(); })
|
|
56
|
+
.catch(function(e) { toast(e.message, 'error'); setSaving(false); });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
var generateScript = function() {
|
|
60
|
+
apiCall('/settings').then(function(settings) {
|
|
61
|
+
var dashUrl = settings?.domainStatus?.url || (typeof location !== 'undefined' ? location.origin : 'https://your-dashboard.agenticmail.io');
|
|
62
|
+
var dbUrl = '<YOUR_DATABASE_URL>';
|
|
63
|
+
var nodeId = form.nodeId || form.name?.toLowerCase().replace(/[^a-z0-9-]/g, '-') || 'worker-1';
|
|
64
|
+
var script = '#!/bin/bash\n' +
|
|
65
|
+
'# AgenticMail Worker Node Setup Script\n' +
|
|
66
|
+
'# Generated for: ' + (form.name || nodeId) + '\n' +
|
|
67
|
+
'# Run this on the target machine\n\n' +
|
|
68
|
+
'set -e\n\n' +
|
|
69
|
+
'# 1. Install Node.js (if not installed)\n' +
|
|
70
|
+
'if ! command -v node &> /dev/null; then\n' +
|
|
71
|
+
' echo "Installing Node.js..."\n' +
|
|
72
|
+
' curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -\n' +
|
|
73
|
+
' sudo apt-get install -y nodejs || brew install node\n' +
|
|
74
|
+
'fi\n\n' +
|
|
75
|
+
'# 2. Install AgenticMail Enterprise\n' +
|
|
76
|
+
'npm install -g @agenticmail/enterprise\n\n' +
|
|
77
|
+
'# 3. Create environment file\n' +
|
|
78
|
+
'mkdir -p ~/.agenticmail\n' +
|
|
79
|
+
'cat > ~/.agenticmail/worker.env << \'ENV\'\n' +
|
|
80
|
+
'ENTERPRISE_URL=' + dashUrl + '\n' +
|
|
81
|
+
'WORKER_NODE_ID=' + nodeId + '\n' +
|
|
82
|
+
'WORKER_NAME="' + (form.name || nodeId) + '"\n' +
|
|
83
|
+
'DATABASE_URL=' + dbUrl + '\n' +
|
|
84
|
+
'PORT=' + (form.port || 3101) + '\n' +
|
|
85
|
+
'LOG_LEVEL=warn\n' +
|
|
86
|
+
'ENV\n\n' +
|
|
87
|
+
'# 4. Install PM2 for process management\n' +
|
|
88
|
+
'npm install -g pm2\n\n' +
|
|
89
|
+
'# 5. Start agent (replace <AGENT_ID> with actual ID)\n' +
|
|
90
|
+
'# pm2 start "agenticmail-enterprise agent --id <AGENT_ID>" --name agent-1 --env-path ~/.agenticmail/worker.env\n\n' +
|
|
91
|
+
'echo ""\n' +
|
|
92
|
+
'echo "Worker node setup complete!"\n' +
|
|
93
|
+
'echo "Edit ~/.agenticmail/worker.env to set DATABASE_URL"\n' +
|
|
94
|
+
'echo "Then start agents with: pm2 start \\"agenticmail-enterprise agent --id <AGENT_ID>\\""\n';
|
|
95
|
+
setScriptGenerated(script);
|
|
96
|
+
}).catch(function() {
|
|
97
|
+
toast('Failed to generate script', 'error');
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
var S = { padding: '8px 12px', borderRadius: 6, border: '1px solid var(--border)', background: 'var(--bg-primary)', color: 'var(--text-primary)', width: '100%', fontSize: 13 };
|
|
102
|
+
|
|
103
|
+
return h('div', { className: 'modal-overlay', onClick: function(e) { if (e.target.className === 'modal-overlay') onClose(); } },
|
|
104
|
+
h('div', { className: 'modal', style: { maxWidth: 640 } },
|
|
105
|
+
h('div', { className: 'modal-header' },
|
|
106
|
+
h('h3', null, 'Add Worker Node'),
|
|
107
|
+
h('button', { className: 'btn btn-ghost btn-sm', onClick: onClose }, 'X')
|
|
108
|
+
),
|
|
109
|
+
h('div', { className: 'modal-body' },
|
|
110
|
+
// Tabs
|
|
111
|
+
h('div', { style: { display: 'flex', gap: 0, marginBottom: 20, borderBottom: '1px solid var(--border)' } },
|
|
112
|
+
['manual', 'ssh', 'script'].map(function(t) {
|
|
113
|
+
var labels = { manual: 'Manual Registration', ssh: 'Deploy via SSH', script: 'Setup Script' };
|
|
114
|
+
return h('button', {
|
|
115
|
+
key: t,
|
|
116
|
+
style: { padding: '8px 16px', border: 'none', background: 'none', cursor: 'pointer', fontSize: 13, fontWeight: tab === t ? 600 : 400,
|
|
117
|
+
color: tab === t ? 'var(--accent-text)' : 'var(--text-muted)', borderBottom: tab === t ? '2px solid var(--accent)' : '2px solid transparent' },
|
|
118
|
+
onClick: function() { setTab(t); }
|
|
119
|
+
}, labels[t]);
|
|
120
|
+
})
|
|
121
|
+
),
|
|
122
|
+
|
|
123
|
+
tab === 'manual' && h(Fragment, null,
|
|
124
|
+
h('p', { style: { fontSize: 13, color: 'var(--text-muted)', marginBottom: 16 } },
|
|
125
|
+
'Register an existing machine that\'s already running AgenticMail. The node will appear in the cluster and start reporting status.'
|
|
126
|
+
),
|
|
127
|
+
h('div', { style: { display: 'grid', gap: 12 } },
|
|
128
|
+
h('div', null,
|
|
129
|
+
h('label', { style: { fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 } }, 'Node Name'),
|
|
130
|
+
h('input', { style: S, value: form.name, placeholder: 'e.g., Office Mac Mini', onChange: function(e) { set('name', e.target.value); } })
|
|
131
|
+
),
|
|
132
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 120px', gap: 8 } },
|
|
133
|
+
h('div', null,
|
|
134
|
+
h('label', { style: { fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 } }, 'Host IP / Hostname *'),
|
|
135
|
+
h('input', { style: S, value: form.host, placeholder: '192.168.1.50 or worker.example.com', onChange: function(e) { set('host', e.target.value); } })
|
|
136
|
+
),
|
|
137
|
+
h('div', null,
|
|
138
|
+
h('label', { style: { fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 } }, 'Port'),
|
|
139
|
+
h('input', { style: S, type: 'number', value: form.port, onChange: function(e) { set('port', e.target.value); } })
|
|
140
|
+
)
|
|
141
|
+
),
|
|
142
|
+
h('div', null,
|
|
143
|
+
h('label', { style: { fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 } }, 'Node ID'),
|
|
144
|
+
h('input', { style: S, value: form.nodeId, placeholder: 'Auto-generated from hostname', onChange: function(e) { set('nodeId', e.target.value); } })
|
|
145
|
+
),
|
|
146
|
+
// Test connection button
|
|
147
|
+
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
148
|
+
h('button', { className: 'btn btn-secondary btn-sm', onClick: testConnection, disabled: testing || !form.host }, testing ? 'Testing...' : 'Test Connection'),
|
|
149
|
+
testResult && h('span', { style: { fontSize: 12, color: testResult.success ? 'var(--accent-green)' : 'var(--accent-red)' } },
|
|
150
|
+
testResult.success ? 'Connected! ' + (testResult.version || '') : 'Failed: ' + (testResult.error || 'unreachable')
|
|
151
|
+
)
|
|
152
|
+
),
|
|
153
|
+
),
|
|
154
|
+
h('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 20 } },
|
|
155
|
+
h('button', { className: 'btn btn-secondary', onClick: onClose }, 'Cancel'),
|
|
156
|
+
h('button', { className: 'btn btn-primary', onClick: saveManual, disabled: saving }, saving ? 'Adding...' : 'Add Node')
|
|
157
|
+
)
|
|
158
|
+
),
|
|
159
|
+
|
|
160
|
+
tab === 'ssh' && h(Fragment, null,
|
|
161
|
+
h('p', { style: { fontSize: 13, color: 'var(--text-muted)', marginBottom: 16 } },
|
|
162
|
+
'Automatically install AgenticMail and configure a worker node on a remote machine via SSH. The dashboard will SSH in, install dependencies, and start the agent process.'
|
|
163
|
+
),
|
|
164
|
+
h('div', { style: { display: 'grid', gap: 12 } },
|
|
165
|
+
h('div', null,
|
|
166
|
+
h('label', { style: { fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 } }, 'Node Name'),
|
|
167
|
+
h('input', { style: S, value: form.name, placeholder: 'e.g., AWS Instance 1', onChange: function(e) { set('name', e.target.value); } })
|
|
168
|
+
),
|
|
169
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 120px', gap: 8 } },
|
|
170
|
+
h('div', null,
|
|
171
|
+
h('label', { style: { fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 } }, 'SSH Host *'),
|
|
172
|
+
h('input', { style: S, value: form.sshHost, placeholder: '1.2.3.4 or server.example.com', onChange: function(e) { set('sshHost', e.target.value); } })
|
|
173
|
+
),
|
|
174
|
+
h('div', null,
|
|
175
|
+
h('label', { style: { fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 } }, 'SSH User'),
|
|
176
|
+
h('input', { style: S, value: form.sshUser, onChange: function(e) { set('sshUser', e.target.value); } })
|
|
177
|
+
)
|
|
178
|
+
),
|
|
179
|
+
h('div', null,
|
|
180
|
+
h('label', { style: { fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 } }, 'SSH Private Key (paste or leave blank for default ~/.ssh/id_rsa)'),
|
|
181
|
+
h('textarea', { style: Object.assign({}, S, { height: 80, fontFamily: 'monospace', fontSize: 11 }), value: form.sshKey, placeholder: '-----BEGIN OPENSSH PRIVATE KEY-----\n...', onChange: function(e) { set('sshKey', e.target.value); } })
|
|
182
|
+
),
|
|
183
|
+
h('div', null,
|
|
184
|
+
h('label', { style: { fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 } }, 'Agent IDs to deploy (comma-separated, or leave blank)'),
|
|
185
|
+
h('input', { style: S, value: form.agentIds, placeholder: 'agent-uuid-1, agent-uuid-2', onChange: function(e) { set('agentIds', e.target.value); } })
|
|
186
|
+
),
|
|
187
|
+
),
|
|
188
|
+
h('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 20 } },
|
|
189
|
+
h('button', { className: 'btn btn-secondary', onClick: onClose }, 'Cancel'),
|
|
190
|
+
h('button', { className: 'btn btn-primary', onClick: deploySsh, disabled: saving || !form.sshHost }, saving ? 'Deploying...' : 'Deploy Worker')
|
|
191
|
+
)
|
|
192
|
+
),
|
|
193
|
+
|
|
194
|
+
tab === 'script' && h(Fragment, null,
|
|
195
|
+
h('p', { style: { fontSize: 13, color: 'var(--text-muted)', marginBottom: 16 } },
|
|
196
|
+
'Generate a setup script to run on the target machine. Copy the script, SSH into the machine, and paste it.'
|
|
197
|
+
),
|
|
198
|
+
h('div', { style: { display: 'grid', gap: 12 } },
|
|
199
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 } },
|
|
200
|
+
h('div', null,
|
|
201
|
+
h('label', { style: { fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 } }, 'Node Name'),
|
|
202
|
+
h('input', { style: S, value: form.name, placeholder: 'my-worker', onChange: function(e) { set('name', e.target.value); } })
|
|
203
|
+
),
|
|
204
|
+
h('div', null,
|
|
205
|
+
h('label', { style: { fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 } }, 'Port'),
|
|
206
|
+
h('input', { style: S, type: 'number', value: form.port, onChange: function(e) { set('port', e.target.value); } })
|
|
207
|
+
)
|
|
208
|
+
),
|
|
209
|
+
h('button', { className: 'btn btn-primary btn-sm', onClick: generateScript }, 'Generate Setup Script'),
|
|
210
|
+
scriptGenerated && h(Fragment, null,
|
|
211
|
+
h('div', { style: { position: 'relative' } },
|
|
212
|
+
h('pre', { style: { background: 'var(--bg-primary)', border: '1px solid var(--border)', borderRadius: 8, padding: 12, fontSize: 11, maxHeight: 300, overflow: 'auto', whiteSpace: 'pre-wrap' } }, scriptGenerated),
|
|
213
|
+
h('button', { className: 'btn btn-secondary btn-sm', style: { position: 'absolute', top: 8, right: 8 },
|
|
214
|
+
onClick: function() { navigator.clipboard.writeText(scriptGenerated); toast('Script copied to clipboard', 'success'); }
|
|
215
|
+
}, 'Copy')
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
),
|
|
219
|
+
h('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 20 } },
|
|
220
|
+
h('button', { className: 'btn btn-secondary', onClick: onClose }, 'Close')
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function NodeDetailModal({ node, onClose, onRefresh, toast }) {
|
|
229
|
+
var [pinging, setPinging] = useState(false);
|
|
230
|
+
var [pingResult, setPingResult] = useState(null);
|
|
231
|
+
var [agents, setAgents] = useState([]);
|
|
232
|
+
|
|
233
|
+
useEffect(function() {
|
|
234
|
+
// Fetch agent details for this node
|
|
235
|
+
if (node.agents && node.agents.length > 0) {
|
|
236
|
+
engineCall('/agents').then(function(d) {
|
|
237
|
+
var all = d.agents || d || [];
|
|
238
|
+
setAgents(all.filter(function(a) { return node.agents.indexOf(a.id) >= 0; }));
|
|
239
|
+
}).catch(function() {});
|
|
240
|
+
}
|
|
241
|
+
}, [node.nodeId]);
|
|
242
|
+
|
|
243
|
+
var pingNode = function() {
|
|
244
|
+
setPinging(true); setPingResult(null);
|
|
245
|
+
engineCall('/cluster/test-connection', {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
body: JSON.stringify({ host: node.host, port: node.port }),
|
|
248
|
+
}).then(function(d) { setPingResult(d); setPinging(false); })
|
|
249
|
+
.catch(function(e) { setPingResult({ success: false, error: e.message }); setPinging(false); });
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
var restartNode = function() {
|
|
253
|
+
if (!confirm('Restart all agents on ' + node.name + '?')) return;
|
|
254
|
+
engineCall('/cluster/nodes/' + node.nodeId + '/restart', { method: 'POST' })
|
|
255
|
+
.then(function() { toast('Restart signal sent', 'success'); })
|
|
256
|
+
.catch(function(e) { toast(e.message, 'error'); });
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
var statusColor = { online: 'var(--accent-green)', degraded: 'var(--accent-orange)', offline: 'var(--text-muted)' }[node.status] || 'var(--text-muted)';
|
|
260
|
+
var uptime = node.onlineSince ? Math.floor((Date.now() - new Date(node.registeredAt).getTime()) / 86400000) + ' days' : '-';
|
|
261
|
+
|
|
262
|
+
return h('div', { className: 'modal-overlay', onClick: function(e) { if (e.target.className === 'modal-overlay') onClose(); } },
|
|
263
|
+
h('div', { className: 'modal', style: { maxWidth: 600 } },
|
|
264
|
+
h('div', { className: 'modal-header' },
|
|
265
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
266
|
+
h('span', { style: { width: 10, height: 10, borderRadius: '50%', background: statusColor, display: 'inline-block' } }),
|
|
267
|
+
h('h3', { style: { margin: 0 } }, node.name || node.nodeId)
|
|
268
|
+
),
|
|
269
|
+
h('button', { className: 'btn btn-ghost btn-sm', onClick: onClose }, 'X')
|
|
270
|
+
),
|
|
271
|
+
h('div', { className: 'modal-body' },
|
|
272
|
+
// Info grid
|
|
273
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16, marginBottom: 20 } },
|
|
274
|
+
h('div', null, h('div', { style: { fontSize: 11, color: 'var(--text-muted)' } }, 'Platform'), h('div', { style: { fontWeight: 600 } }, node.platform + '/' + node.arch)),
|
|
275
|
+
h('div', null, h('div', { style: { fontSize: 11, color: 'var(--text-muted)' } }, 'CPUs'), h('div', { style: { fontWeight: 600 } }, node.cpuCount)),
|
|
276
|
+
h('div', null, h('div', { style: { fontSize: 11, color: 'var(--text-muted)' } }, 'Memory'), h('div', { style: { fontWeight: 600 } }, node.memoryMb >= 1024 ? (node.memoryMb / 1024).toFixed(1) + ' GB' : node.memoryMb + ' MB')),
|
|
277
|
+
h('div', null, h('div', { style: { fontSize: 11, color: 'var(--text-muted)' } }, 'Address'), h('div', { style: { fontWeight: 600, fontFamily: 'monospace', fontSize: 12 } }, node.host + ':' + node.port)),
|
|
278
|
+
h('div', null, h('div', { style: { fontSize: 11, color: 'var(--text-muted)' } }, 'Version'), h('div', { style: { fontWeight: 600 } }, 'v' + (node.version || '?'))),
|
|
279
|
+
h('div', null, h('div', { style: { fontSize: 11, color: 'var(--text-muted)' } }, 'Registered'), h('div', { style: { fontWeight: 600 } }, node.registeredAt ? new Date(node.registeredAt).toLocaleDateString() : '-')),
|
|
280
|
+
),
|
|
281
|
+
|
|
282
|
+
// Capabilities
|
|
283
|
+
node.capabilities && node.capabilities.length > 0 && h('div', { style: { marginBottom: 16 } },
|
|
284
|
+
h('div', { style: { fontSize: 12, fontWeight: 600, marginBottom: 6 } }, 'Capabilities'),
|
|
285
|
+
h('div', { style: { display: 'flex', gap: 4, flexWrap: 'wrap' } },
|
|
286
|
+
node.capabilities.map(function(c) { return h('span', { key: c, className: 'badge badge-neutral' }, c); })
|
|
287
|
+
)
|
|
288
|
+
),
|
|
289
|
+
|
|
290
|
+
// Agents on this node
|
|
291
|
+
h('div', { style: { marginBottom: 16 } },
|
|
292
|
+
h('div', { style: { fontSize: 12, fontWeight: 600, marginBottom: 6 } }, 'Agents (' + (node.agents ? node.agents.length : 0) + ')'),
|
|
293
|
+
node.agents && node.agents.length > 0
|
|
294
|
+
? h('div', { style: { display: 'grid', gap: 6 } },
|
|
295
|
+
agents.length > 0
|
|
296
|
+
? agents.map(function(a) {
|
|
297
|
+
return h('div', { key: a.id, style: { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', background: 'var(--bg-secondary)', borderRadius: 6 } },
|
|
298
|
+
h('span', { style: { fontWeight: 600, fontSize: 13 } }, a.name || a.id),
|
|
299
|
+
h('span', { style: { fontSize: 11, color: 'var(--text-muted)', fontFamily: 'monospace' } }, a.email || ''),
|
|
300
|
+
h('span', { className: 'badge badge-neutral', style: { marginLeft: 'auto' } }, a.role || 'agent')
|
|
301
|
+
);
|
|
302
|
+
})
|
|
303
|
+
: node.agents.map(function(id) {
|
|
304
|
+
return h('div', { key: id, style: { padding: '6px 10px', background: 'var(--bg-secondary)', borderRadius: 6, fontFamily: 'monospace', fontSize: 12 } }, id);
|
|
305
|
+
})
|
|
306
|
+
)
|
|
307
|
+
: h('div', { style: { fontSize: 13, color: 'var(--text-muted)', fontStyle: 'italic' } }, 'No agents running on this node')
|
|
308
|
+
),
|
|
309
|
+
|
|
310
|
+
// Actions
|
|
311
|
+
h('div', { style: { display: 'flex', gap: 8, paddingTop: 12, borderTop: '1px solid var(--border)' } },
|
|
312
|
+
h('button', { className: 'btn btn-secondary btn-sm', onClick: pingNode, disabled: pinging }, pinging ? 'Pinging...' : 'Ping Node'),
|
|
313
|
+
pingResult && h('span', { style: { fontSize: 12, color: pingResult.success ? 'var(--accent-green)' : 'var(--accent-red)', alignSelf: 'center' } },
|
|
314
|
+
pingResult.success ? 'Reachable (' + (pingResult.latencyMs || '?') + 'ms)' : 'Unreachable: ' + (pingResult.error || '')
|
|
315
|
+
),
|
|
316
|
+
h('div', { style: { flex: 1 } }),
|
|
317
|
+
h('button', { className: 'btn btn-secondary btn-sm', onClick: restartNode }, 'Restart Agents'),
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function ClusterPage() {
|
|
325
|
+
var app = useApp();
|
|
326
|
+
var toast = app.toast;
|
|
327
|
+
var [nodes, setNodes] = useState([]);
|
|
328
|
+
var [stats, setStats] = useState(null);
|
|
329
|
+
var [loading, setLoading] = useState(true);
|
|
330
|
+
var [addingNode, setAddingNode] = useState(false);
|
|
331
|
+
var [selectedNode, setSelectedNode] = useState(null);
|
|
332
|
+
|
|
333
|
+
var load = function() {
|
|
334
|
+
engineCall('/cluster/nodes').then(function(d) {
|
|
335
|
+
setNodes(d.nodes || []);
|
|
336
|
+
setStats(d.stats || null);
|
|
337
|
+
setLoading(false);
|
|
338
|
+
}).catch(function() { setLoading(false); });
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
useEffect(function() { load(); }, []);
|
|
342
|
+
|
|
343
|
+
// Real-time updates via SSE
|
|
344
|
+
useEffect(function() {
|
|
345
|
+
var es = new EventSource('/api/engine/cluster/stream');
|
|
346
|
+
es.onmessage = function(ev) {
|
|
347
|
+
try {
|
|
348
|
+
var d = JSON.parse(ev.data);
|
|
349
|
+
if (d.type === 'node') {
|
|
350
|
+
setNodes(function(prev) {
|
|
351
|
+
var idx = prev.findIndex(function(n) { return n.nodeId === d.nodeId; });
|
|
352
|
+
var next = prev.slice();
|
|
353
|
+
if (d.event === 'offline' && idx >= 0) { next[idx] = Object.assign({}, next[idx], { status: 'offline', agents: [] }); }
|
|
354
|
+
else if (idx >= 0) { next[idx] = d; }
|
|
355
|
+
else if (d.event === 'register' || d.event === 'snapshot') { next.push(d); }
|
|
356
|
+
return next;
|
|
357
|
+
});
|
|
358
|
+
// Refresh stats
|
|
359
|
+
engineCall('/cluster/nodes').then(function(dd) { setStats(dd.stats || null); }).catch(function() {});
|
|
360
|
+
}
|
|
361
|
+
} catch(e) {}
|
|
362
|
+
};
|
|
363
|
+
return function() { es.close(); };
|
|
364
|
+
}, []);
|
|
365
|
+
|
|
366
|
+
var removeNode = function(e, nodeId) {
|
|
367
|
+
e.stopPropagation();
|
|
368
|
+
if (!confirm('Remove worker node "' + nodeId + '"? This only removes it from the dashboard — agents on it will keep running but stop reporting.')) return;
|
|
369
|
+
engineCall('/cluster/nodes/' + nodeId, { method: 'DELETE' }).then(function() {
|
|
370
|
+
toast('Node removed', 'success');
|
|
371
|
+
load();
|
|
372
|
+
}).catch(function(e) { toast(e.message, 'error'); });
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
var statusColor = function(s) { return { online: 'success', degraded: 'warning', offline: 'neutral' }[s] || 'neutral'; };
|
|
376
|
+
var statusDot = function(s) { return { online: 'var(--accent-green)', degraded: 'var(--accent-orange)', offline: 'var(--text-muted)' }[s] || 'var(--text-muted)'; };
|
|
377
|
+
|
|
378
|
+
var formatBytes = function(mb) {
|
|
379
|
+
if (mb >= 1024) return (mb / 1024).toFixed(1) + ' GB';
|
|
380
|
+
return mb + ' MB';
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
var timeSince = function(iso) {
|
|
384
|
+
if (!iso) return 'never';
|
|
385
|
+
var s = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
|
|
386
|
+
if (s < 60) return s + 's ago';
|
|
387
|
+
if (s < 3600) return Math.floor(s / 60) + 'm ago';
|
|
388
|
+
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
|
|
389
|
+
return Math.floor(s / 86400) + 'd ago';
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
if (loading) return h('div', { style: { padding: 40, textAlign: 'center', color: 'var(--text-muted)' } }, 'Loading cluster...');
|
|
393
|
+
|
|
394
|
+
return h(Fragment, null,
|
|
395
|
+
addingNode && h(AddNodeModal, { onClose: function() { setAddingNode(false); }, onAdded: load, toast: toast }),
|
|
396
|
+
selectedNode && h(NodeDetailModal, { node: selectedNode, onClose: function() { setSelectedNode(null); }, onRefresh: load, toast: toast }),
|
|
397
|
+
|
|
398
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 } },
|
|
399
|
+
h('div', null,
|
|
400
|
+
h('h1', { style: { fontSize: 20, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 8 } }, 'Cluster', h(KnowledgeLink, { page: 'cluster' }),
|
|
401
|
+
h(HelpButton, { label: 'Cluster' },
|
|
402
|
+
h('p', null, 'Manage worker nodes running agents across multiple machines. Scale horizontally by adding machines — each one runs agents that report back to this dashboard.'),
|
|
403
|
+
h('p', { style: { marginTop: 8 } }, h('strong', null, '3 ways to add a worker:'),
|
|
404
|
+
h('ul', { style: { paddingLeft: 16, marginTop: 4 } },
|
|
405
|
+
h('li', null, h('strong', null, 'Manual'), ' — Register an existing machine by IP/hostname'),
|
|
406
|
+
h('li', null, h('strong', null, 'SSH Deploy'), ' — Auto-install on a remote machine via SSH'),
|
|
407
|
+
h('li', null, h('strong', null, 'Setup Script'), ' — Generate a script to run on the target machine')
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
),
|
|
412
|
+
h('p', { style: { color: 'var(--text-muted)', fontSize: 13 } }, 'Scale your AI workforce across multiple machines')
|
|
413
|
+
),
|
|
414
|
+
h('div', { style: { display: 'flex', gap: 8 } },
|
|
415
|
+
h('button', { className: 'btn btn-secondary btn-sm', onClick: load }, I.refresh(), ' Refresh'),
|
|
416
|
+
h('button', { className: 'btn btn-primary', onClick: function() { setAddingNode(true); } }, I.plus(), ' Add Worker Node')
|
|
417
|
+
)
|
|
418
|
+
),
|
|
419
|
+
|
|
420
|
+
// Stats cards
|
|
421
|
+
stats && h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 12, marginBottom: 20 } },
|
|
422
|
+
h('div', { className: 'card' }, h('div', { className: 'card-body', style: { padding: 16, textAlign: 'center' } },
|
|
423
|
+
h('div', { style: { fontSize: 28, fontWeight: 700 } }, stats.totalNodes),
|
|
424
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)' } }, 'Total Nodes')
|
|
425
|
+
)),
|
|
426
|
+
h('div', { className: 'card' }, h('div', { className: 'card-body', style: { padding: 16, textAlign: 'center' } },
|
|
427
|
+
h('div', { style: { fontSize: 28, fontWeight: 700, color: 'var(--accent-green)' } }, stats.onlineNodes),
|
|
428
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)' } }, 'Online')
|
|
429
|
+
)),
|
|
430
|
+
h('div', { className: 'card' }, h('div', { className: 'card-body', style: { padding: 16, textAlign: 'center' } },
|
|
431
|
+
h('div', { style: { fontSize: 28, fontWeight: 700 } }, stats.totalAgents),
|
|
432
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)' } }, 'Running Agents')
|
|
433
|
+
)),
|
|
434
|
+
h('div', { className: 'card' }, h('div', { className: 'card-body', style: { padding: 16, textAlign: 'center' } },
|
|
435
|
+
h('div', { style: { fontSize: 28, fontWeight: 700 } }, stats.totalCpus),
|
|
436
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)' } }, 'Total CPUs')
|
|
437
|
+
)),
|
|
438
|
+
h('div', { className: 'card' }, h('div', { className: 'card-body', style: { padding: 16, textAlign: 'center' } },
|
|
439
|
+
h('div', { style: { fontSize: 28, fontWeight: 700 } }, formatBytes(stats.totalMemoryMb)),
|
|
440
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)' } }, 'Total Memory')
|
|
441
|
+
))
|
|
442
|
+
),
|
|
443
|
+
|
|
444
|
+
// Nodes
|
|
445
|
+
nodes.length === 0
|
|
446
|
+
? h('div', { className: 'card' }, h('div', { className: 'card-body', style: { padding: 40 } },
|
|
447
|
+
h('div', { className: 'empty-state' },
|
|
448
|
+
I.server(),
|
|
449
|
+
h('h3', null, 'No worker nodes'),
|
|
450
|
+
h('p', null, 'Your agents currently run on this machine. Add worker nodes to distribute agents across multiple machines for horizontal scaling.'),
|
|
451
|
+
h('button', { className: 'btn btn-primary', style: { marginTop: 12 }, onClick: function() { setAddingNode(true); } }, I.plus(), ' Add Your First Worker Node')
|
|
452
|
+
)
|
|
453
|
+
))
|
|
454
|
+
: h('div', { style: { display: 'grid', gap: 12 } },
|
|
455
|
+
nodes.map(function(node) {
|
|
456
|
+
return h('div', { key: node.nodeId, className: 'card', style: { cursor: 'pointer', transition: 'border-color 0.2s' },
|
|
457
|
+
onClick: function() { setSelectedNode(node); },
|
|
458
|
+
onMouseEnter: function(e) { e.currentTarget.style.borderColor = 'var(--accent)'; },
|
|
459
|
+
onMouseLeave: function(e) { e.currentTarget.style.borderColor = ''; }
|
|
460
|
+
},
|
|
461
|
+
h('div', { className: 'card-body', style: { padding: 16 } },
|
|
462
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' } },
|
|
463
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
|
|
464
|
+
// Status dot
|
|
465
|
+
h('span', { style: { width: 12, height: 12, borderRadius: '50%', background: statusDot(node.status), flexShrink: 0, boxShadow: node.status === 'online' ? '0 0 6px ' + statusDot(node.status) : 'none' } }),
|
|
466
|
+
h('div', null,
|
|
467
|
+
h('div', { style: { fontSize: 15, fontWeight: 700 } }, node.name || node.nodeId),
|
|
468
|
+
h('div', { style: { fontSize: 12, color: 'var(--text-muted)', fontFamily: 'monospace' } }, node.host + ':' + node.port)
|
|
469
|
+
)
|
|
470
|
+
),
|
|
471
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
472
|
+
h('span', { className: 'badge badge-' + statusColor(node.status), style: { textTransform: 'capitalize' } }, node.status),
|
|
473
|
+
h('button', { className: 'btn btn-ghost btn-sm', onClick: function(e) { removeNode(e, node.nodeId); }, title: 'Remove node' }, I.trash())
|
|
474
|
+
)
|
|
475
|
+
),
|
|
476
|
+
// Metrics row
|
|
477
|
+
h('div', { style: { display: 'flex', gap: 24, marginTop: 12, paddingTop: 12, borderTop: '1px solid var(--border)' } },
|
|
478
|
+
h('div', null,
|
|
479
|
+
h('div', { style: { fontSize: 10, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5 } }, 'Platform'),
|
|
480
|
+
h('div', { style: { fontWeight: 600, fontSize: 13 } }, node.platform + '/' + node.arch)
|
|
481
|
+
),
|
|
482
|
+
h('div', null,
|
|
483
|
+
h('div', { style: { fontSize: 10, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5 } }, 'CPUs'),
|
|
484
|
+
h('div', { style: { fontWeight: 600, fontSize: 13 } }, node.cpuCount || '-')
|
|
485
|
+
),
|
|
486
|
+
h('div', null,
|
|
487
|
+
h('div', { style: { fontSize: 10, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5 } }, 'Memory'),
|
|
488
|
+
h('div', { style: { fontWeight: 600, fontSize: 13 } }, formatBytes(node.memoryMb))
|
|
489
|
+
),
|
|
490
|
+
h('div', null,
|
|
491
|
+
h('div', { style: { fontSize: 10, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5 } }, 'Agents'),
|
|
492
|
+
h('div', { style: { fontWeight: 600, fontSize: 13 } }, node.agents ? node.agents.length : 0)
|
|
493
|
+
),
|
|
494
|
+
h('div', null,
|
|
495
|
+
h('div', { style: { fontSize: 10, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5 } }, 'Version'),
|
|
496
|
+
h('div', { style: { fontWeight: 600, fontSize: 13 } }, 'v' + (node.version || '?'))
|
|
497
|
+
),
|
|
498
|
+
h('div', null,
|
|
499
|
+
h('div', { style: { fontSize: 10, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5 } }, 'Last Seen'),
|
|
500
|
+
h('div', { style: { fontWeight: 600, fontSize: 13, color: node.status === 'online' ? 'var(--accent-green)' : 'var(--text-muted)' } }, timeSince(node.lastHeartbeat))
|
|
501
|
+
)
|
|
502
|
+
),
|
|
503
|
+
// Capabilities
|
|
504
|
+
node.capabilities && node.capabilities.length > 0 && h('div', { style: { display: 'flex', gap: 4, marginTop: 8 } },
|
|
505
|
+
node.capabilities.map(function(c) { return h('span', { key: c, className: 'badge badge-neutral', style: { fontSize: 10 } }, c); })
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
);
|
|
509
|
+
})
|
|
510
|
+
)
|
|
511
|
+
);
|
|
512
|
+
}
|