@swarmclawai/swarmclaw 0.7.6 → 0.7.8
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/README.md +19 -10
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +16 -0
- package/src/app/api/agents/route.ts +2 -0
- package/src/app/api/chats/[id]/route.ts +21 -1
- package/src/app/api/chats/route.ts +13 -1
- package/src/app/api/connectors/[id]/route.ts +20 -2
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
- package/src/app/api/external-agents/[id]/route.ts +38 -6
- package/src/app/api/external-agents/route.ts +17 -1
- package/src/app/api/gateways/[id]/health/route.ts +8 -0
- package/src/app/api/gateways/[id]/route.ts +53 -1
- package/src/app/api/gateways/route.ts +53 -0
- package/src/app/api/openclaw/deploy/route.ts +139 -0
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/cli/index.js +40 -0
- package/src/cli/index.test.js +68 -0
- package/src/cli/spec.js +60 -0
- package/src/components/agents/agent-sheet.tsx +281 -33
- package/src/components/auth/setup-wizard.tsx +75 -2
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-header.tsx +4 -0
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/gateways/gateway-sheet.tsx +140 -8
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +221 -17
- package/src/components/shared/settings/section-capability-policy.tsx +38 -0
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/tasks/approvals-panel.tsx +177 -18
- package/src/components/tasks/task-board.tsx +137 -23
- package/src/components/tasks/task-card.tsx +29 -0
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/lib/server/agent-runtime-config.ts +142 -7
- package/src/lib/server/agent-thread-session.ts +9 -1
- package/src/lib/server/capability-router.test.ts +22 -0
- package/src/lib/server/capability-router.ts +54 -18
- package/src/lib/server/chat-execution.ts +33 -3
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.ts +99 -74
- package/src/lib/server/daemon-state.ts +83 -46
- package/src/lib/server/elevenlabs.test.ts +59 -1
- package/src/lib/server/heartbeat-service.ts +5 -1
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/openclaw-deploy.test.ts +8 -0
- package/src/lib/server/openclaw-deploy.ts +679 -19
- package/src/lib/server/orchestrator-lg.ts +1 -0
- package/src/lib/server/orchestrator.ts +11 -0
- package/src/lib/server/plugins.ts +6 -1
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-followups.test.ts +147 -2
- package/src/lib/server/queue.ts +278 -8
- package/src/lib/server/session-run-manager.ts +31 -0
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.ts +26 -1
- package/src/lib/server/session-tools/context.ts +5 -0
- package/src/lib/server/session-tools/crud.ts +265 -76
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +38 -2
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.ts +14 -2
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
- package/src/lib/server/session-tools/web.ts +153 -6
- package/src/lib/server/stream-agent-chat.test.ts +27 -2
- package/src/lib/server/stream-agent-chat.ts +104 -30
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +269 -0
- package/src/lib/setup-defaults.ts +2 -2
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/validation/schemas.ts +9 -0
- package/src/types/index.ts +104 -0
|
@@ -6,6 +6,8 @@ import { copyTextToClipboard } from '@/lib/clipboard'
|
|
|
6
6
|
|
|
7
7
|
type RemoteTemplate = 'docker' | 'render' | 'fly' | 'railway'
|
|
8
8
|
type RemoteProvider = 'hetzner' | 'digitalocean' | 'vultr' | 'linode' | 'lightsail' | 'gcp' | 'azure' | 'oci' | 'generic'
|
|
9
|
+
type UseCaseTemplate = 'local-dev' | 'single-vps' | 'private-tailnet' | 'browser-heavy' | 'team-control'
|
|
10
|
+
type ExposurePreset = 'private-lan' | 'tailscale' | 'caddy' | 'nginx' | 'ssh-tunnel'
|
|
9
11
|
|
|
10
12
|
interface LocalDeployStatus {
|
|
11
13
|
running: boolean
|
|
@@ -22,6 +24,22 @@ interface LocalDeployStatus {
|
|
|
22
24
|
installCommand: string
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
interface RemoteDeployStatus {
|
|
28
|
+
active: boolean
|
|
29
|
+
processId: string | null
|
|
30
|
+
pid: number | null
|
|
31
|
+
action: string | null
|
|
32
|
+
target: string | null
|
|
33
|
+
startedAt: number | null
|
|
34
|
+
status: 'idle' | 'running' | 'exited' | 'killed' | 'failed' | 'timeout'
|
|
35
|
+
exitCode: number | null
|
|
36
|
+
tail: string
|
|
37
|
+
lastError: string | null
|
|
38
|
+
lastSummary: string | null
|
|
39
|
+
lastCommandPreview: string | null
|
|
40
|
+
lastBackupPath: string | null
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
interface DeployFile {
|
|
26
44
|
name: string
|
|
27
45
|
language: 'bash' | 'yaml' | 'env' | 'toml' | 'text'
|
|
@@ -32,6 +50,8 @@ interface DeployBundle {
|
|
|
32
50
|
template: RemoteTemplate
|
|
33
51
|
provider: RemoteProvider
|
|
34
52
|
providerLabel: string
|
|
53
|
+
useCase: UseCaseTemplate
|
|
54
|
+
exposure: ExposurePreset
|
|
35
55
|
title: string
|
|
36
56
|
summary: string
|
|
37
57
|
endpoint: string
|
|
@@ -43,6 +63,7 @@ interface DeployBundle {
|
|
|
43
63
|
|
|
44
64
|
interface DeployStatusResponse {
|
|
45
65
|
local: LocalDeployStatus
|
|
66
|
+
remote?: RemoteDeployStatus
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
interface DeployActionResponse {
|
|
@@ -50,6 +71,19 @@ interface DeployActionResponse {
|
|
|
50
71
|
local?: LocalDeployStatus
|
|
51
72
|
token?: string
|
|
52
73
|
bundle?: DeployBundle
|
|
74
|
+
processId?: string | null
|
|
75
|
+
remote?: RemoteDeployStatus
|
|
76
|
+
summary?: string
|
|
77
|
+
commandPreview?: string
|
|
78
|
+
verify?: {
|
|
79
|
+
ok: boolean
|
|
80
|
+
endpoint: string
|
|
81
|
+
wsUrl: string
|
|
82
|
+
authProvided: boolean
|
|
83
|
+
models: string[]
|
|
84
|
+
error?: string
|
|
85
|
+
hint?: string
|
|
86
|
+
}
|
|
53
87
|
error?: string
|
|
54
88
|
}
|
|
55
89
|
|
|
@@ -58,11 +92,32 @@ interface ApplyPatch {
|
|
|
58
92
|
token?: string
|
|
59
93
|
name?: string
|
|
60
94
|
notes?: string
|
|
95
|
+
deployment?: {
|
|
96
|
+
method?: 'local' | 'bundle' | 'ssh' | 'imported' | null
|
|
97
|
+
provider?: string | null
|
|
98
|
+
remoteTarget?: RemoteTemplate | null
|
|
99
|
+
useCase?: UseCaseTemplate | null
|
|
100
|
+
exposure?: ExposurePreset | null
|
|
101
|
+
sshHost?: string | null
|
|
102
|
+
sshUser?: string | null
|
|
103
|
+
sshPort?: number | null
|
|
104
|
+
sshKeyPath?: string | null
|
|
105
|
+
sshTargetDir?: string | null
|
|
106
|
+
lastDeployAt?: number | null
|
|
107
|
+
lastDeployAction?: string | null
|
|
108
|
+
lastDeploySummary?: string | null
|
|
109
|
+
lastDeployProcessId?: string | null
|
|
110
|
+
lastVerifiedAt?: number | null
|
|
111
|
+
lastVerifiedOk?: boolean | null
|
|
112
|
+
lastVerifiedMessage?: string | null
|
|
113
|
+
lastBackupPath?: string | null
|
|
114
|
+
}
|
|
61
115
|
}
|
|
62
116
|
|
|
63
117
|
interface OpenClawDeployPanelProps {
|
|
64
118
|
endpoint?: string | null
|
|
65
119
|
token?: string | null
|
|
120
|
+
deployment?: ApplyPatch['deployment'] | null
|
|
66
121
|
suggestedName?: string | null
|
|
67
122
|
title?: string
|
|
68
123
|
description?: string
|
|
@@ -113,6 +168,30 @@ const PROVIDER_OPTIONS: Array<{
|
|
|
113
168
|
{ id: 'generic', label: 'Generic', detail: 'Any Ubuntu 24.04 host' },
|
|
114
169
|
]
|
|
115
170
|
|
|
171
|
+
const USE_CASE_OPTIONS: Array<{
|
|
172
|
+
id: UseCaseTemplate
|
|
173
|
+
label: string
|
|
174
|
+
detail: string
|
|
175
|
+
}> = [
|
|
176
|
+
{ id: 'local-dev', label: 'Local Dev', detail: 'Loopback-friendly defaults for one machine and quick setup.' },
|
|
177
|
+
{ id: 'single-vps', label: 'Single VPS', detail: 'Balanced default for most public or private VPS installs.' },
|
|
178
|
+
{ id: 'private-tailnet', label: 'Private Tailnet', detail: 'Keep the gateway private and expose it over a tailnet.' },
|
|
179
|
+
{ id: 'browser-heavy', label: 'Browser Heavy', detail: 'Roomier defaults for browser-backed nodes and automation.' },
|
|
180
|
+
{ id: 'team-control', label: 'Team Control', detail: 'Shared operator-friendly control plane defaults and backups.' },
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
const EXPOSURE_OPTIONS: Array<{
|
|
184
|
+
id: ExposurePreset
|
|
185
|
+
label: string
|
|
186
|
+
detail: string
|
|
187
|
+
}> = [
|
|
188
|
+
{ id: 'private-lan', label: 'Private LAN', detail: 'Expose on LAN only and rely on your own firewall rules.' },
|
|
189
|
+
{ id: 'tailscale', label: 'Tailscale', detail: 'Loopback only plus a tailnet-facing Tailscale serve script.' },
|
|
190
|
+
{ id: 'caddy', label: 'Caddy', detail: 'Bundled reverse proxy with simple HTTPS termination.' },
|
|
191
|
+
{ id: 'nginx', label: 'Nginx', detail: 'Bundled reverse proxy config for teams with existing TLS handling.' },
|
|
192
|
+
{ id: 'ssh-tunnel', label: 'SSH Tunnel', detail: 'Keep it private and access the gateway through SSH port-forwarding.' },
|
|
193
|
+
]
|
|
194
|
+
|
|
116
195
|
function buildLocalRunCommand(port: number, token?: string | null): string {
|
|
117
196
|
const parts = ['npx', 'openclaw', 'gateway', 'run', '--allow-unconfigured', '--force', '--bind', 'loopback', '--port', String(port)]
|
|
118
197
|
if (token) parts.push('--auth', 'token', '--token', token)
|
|
@@ -170,6 +249,7 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
170
249
|
const {
|
|
171
250
|
endpoint,
|
|
172
251
|
token,
|
|
252
|
+
deployment,
|
|
173
253
|
suggestedName,
|
|
174
254
|
title = 'Smart Deploy OpenClaw',
|
|
175
255
|
description = 'Launch a local gateway on this host or generate a remote bundle with opinionated defaults.',
|
|
@@ -179,6 +259,7 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
179
259
|
|
|
180
260
|
const [activeTab, setActiveTab] = useState<'local' | 'remote'>('local')
|
|
181
261
|
const [localStatus, setLocalStatus] = useState<LocalDeployStatus | null>(null)
|
|
262
|
+
const [remoteStatus, setRemoteStatus] = useState<RemoteDeployStatus | null>(null)
|
|
182
263
|
const [localPort, setLocalPort] = useState(() => inferPort(endpoint))
|
|
183
264
|
const [deployToken, setDeployToken] = useState(token || '')
|
|
184
265
|
const [remoteTarget, setRemoteTarget] = useState(() => inferRemoteTarget(endpoint))
|
|
@@ -187,12 +268,22 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
187
268
|
))
|
|
188
269
|
const [remoteTemplate, setRemoteTemplate] = useState<RemoteTemplate>('docker')
|
|
189
270
|
const [remoteProvider, setRemoteProvider] = useState<RemoteProvider>('hetzner')
|
|
271
|
+
const [useCase, setUseCase] = useState<UseCaseTemplate>(() => deployment?.useCase || 'single-vps')
|
|
272
|
+
const [exposure, setExposure] = useState<ExposurePreset>(() => deployment?.exposure || 'caddy')
|
|
273
|
+
const [sshHost, setSshHost] = useState(() => deployment?.sshHost || inferRemoteTarget(endpoint))
|
|
274
|
+
const [sshUser, setSshUser] = useState(() => deployment?.sshUser || 'root')
|
|
275
|
+
const [sshPort, setSshPort] = useState(() => deployment?.sshPort || 22)
|
|
276
|
+
const [sshKeyPath, setSshKeyPath] = useState(() => deployment?.sshKeyPath || '')
|
|
277
|
+
const [sshTargetDir, setSshTargetDir] = useState(() => deployment?.sshTargetDir || '/opt/openclaw')
|
|
278
|
+
const [restoreBackupPath, setRestoreBackupPath] = useState(() => deployment?.lastBackupPath || '')
|
|
190
279
|
const [bundle, setBundle] = useState<DeployBundle | null>(null)
|
|
191
280
|
const [bundleFile, setBundleFile] = useState('')
|
|
192
|
-
const [loading, setLoading] = useState<'idle' | 'starting-local' | 'stopping-local' | 'generating-bundle'>('idle')
|
|
281
|
+
const [loading, setLoading] = useState<'idle' | 'starting-local' | 'stopping-local' | 'restarting-local' | 'generating-bundle' | 'ssh-deploy' | 'verifying' | 'remote-action'>('idle')
|
|
193
282
|
const [message, setMessage] = useState('')
|
|
194
283
|
const [error, setError] = useState('')
|
|
195
284
|
const [copiedKey, setCopiedKey] = useState('')
|
|
285
|
+
const [commandPreview, setCommandPreview] = useState('')
|
|
286
|
+
const [verifySummary, setVerifySummary] = useState('')
|
|
196
287
|
|
|
197
288
|
useEffect(() => {
|
|
198
289
|
if (token && !deployToken) setDeployToken(token)
|
|
@@ -204,16 +295,30 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
204
295
|
setActiveTab('local')
|
|
205
296
|
} else if (endpoint && inferRemoteTarget(endpoint)) {
|
|
206
297
|
setRemoteTarget(inferRemoteTarget(endpoint))
|
|
298
|
+
setSshHost((current) => current || inferRemoteTarget(endpoint))
|
|
207
299
|
setActiveTab('remote')
|
|
208
300
|
}
|
|
209
301
|
}, [endpoint])
|
|
210
302
|
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
if (!deployment) return
|
|
305
|
+
if (deployment.useCase) setUseCase(deployment.useCase)
|
|
306
|
+
if (deployment.exposure) setExposure(deployment.exposure)
|
|
307
|
+
if (deployment.sshHost) setSshHost(deployment.sshHost)
|
|
308
|
+
if (deployment.sshUser) setSshUser(deployment.sshUser)
|
|
309
|
+
if (deployment.sshPort) setSshPort(deployment.sshPort)
|
|
310
|
+
if (deployment.sshKeyPath) setSshKeyPath(deployment.sshKeyPath)
|
|
311
|
+
if (deployment.sshTargetDir) setSshTargetDir(deployment.sshTargetDir)
|
|
312
|
+
if (deployment.lastBackupPath) setRestoreBackupPath(deployment.lastBackupPath)
|
|
313
|
+
}, [deployment])
|
|
314
|
+
|
|
211
315
|
useEffect(() => {
|
|
212
316
|
let cancelled = false
|
|
213
317
|
api<DeployStatusResponse>('GET', '/openclaw/deploy')
|
|
214
318
|
.then((result) => {
|
|
215
319
|
if (!cancelled) {
|
|
216
320
|
setLocalStatus(result.local)
|
|
321
|
+
setRemoteStatus(result.remote || null)
|
|
217
322
|
if (result.local.token) {
|
|
218
323
|
setDeployToken((current) => current || result.local.token || '')
|
|
219
324
|
}
|
|
@@ -225,6 +330,19 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
225
330
|
}
|
|
226
331
|
}, [])
|
|
227
332
|
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (!remoteStatus?.active) return
|
|
335
|
+
const timer = window.setInterval(() => {
|
|
336
|
+
api<DeployStatusResponse>('GET', '/openclaw/deploy')
|
|
337
|
+
.then((result) => {
|
|
338
|
+
setLocalStatus(result.local)
|
|
339
|
+
setRemoteStatus(result.remote || null)
|
|
340
|
+
})
|
|
341
|
+
.catch(() => {})
|
|
342
|
+
}, 2500)
|
|
343
|
+
return () => window.clearInterval(timer)
|
|
344
|
+
}, [remoteStatus?.active])
|
|
345
|
+
|
|
228
346
|
const selectedFile = useMemo(() => {
|
|
229
347
|
if (!bundle) return null
|
|
230
348
|
return bundle.files.find((file) => file.name === bundleFile) || bundle.files[0] || null
|
|
@@ -259,9 +377,35 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
259
377
|
}, 1200)
|
|
260
378
|
}
|
|
261
379
|
|
|
380
|
+
const applyDeploymentPatch = async (patch: ApplyPatch) => {
|
|
381
|
+
await Promise.resolve(onApply?.(patch))
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const buildRemoteDeploymentPatch = (overrides?: Partial<NonNullable<ApplyPatch['deployment']>>): NonNullable<ApplyPatch['deployment']> => ({
|
|
385
|
+
method: overrides?.method || (remoteTemplate === 'docker' ? 'bundle' : 'bundle'),
|
|
386
|
+
provider: overrides?.provider || (remoteTemplate === 'docker' ? remoteProvider : remoteTemplate),
|
|
387
|
+
remoteTarget: overrides?.remoteTarget || remoteTemplate,
|
|
388
|
+
useCase,
|
|
389
|
+
exposure,
|
|
390
|
+
sshHost: sshHost.trim() || null,
|
|
391
|
+
sshUser: sshUser.trim() || null,
|
|
392
|
+
sshPort,
|
|
393
|
+
sshKeyPath: sshKeyPath.trim() || null,
|
|
394
|
+
sshTargetDir: sshTargetDir.trim() || null,
|
|
395
|
+
lastDeployAt: overrides?.lastDeployAt ?? null,
|
|
396
|
+
lastDeployAction: overrides?.lastDeployAction ?? null,
|
|
397
|
+
lastDeploySummary: overrides?.lastDeploySummary ?? null,
|
|
398
|
+
lastDeployProcessId: overrides?.lastDeployProcessId ?? null,
|
|
399
|
+
lastVerifiedAt: overrides?.lastVerifiedAt ?? null,
|
|
400
|
+
lastVerifiedOk: overrides?.lastVerifiedOk ?? null,
|
|
401
|
+
lastVerifiedMessage: overrides?.lastVerifiedMessage ?? null,
|
|
402
|
+
lastBackupPath: overrides?.lastBackupPath ?? deployment?.lastBackupPath ?? null,
|
|
403
|
+
})
|
|
404
|
+
|
|
262
405
|
const handleStartLocal = async () => {
|
|
263
406
|
setLoading('starting-local')
|
|
264
407
|
setError('')
|
|
408
|
+
setVerifySummary('')
|
|
265
409
|
try {
|
|
266
410
|
const result = await api<DeployActionResponse>('POST', '/openclaw/deploy', {
|
|
267
411
|
action: 'start-local',
|
|
@@ -271,12 +415,36 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
271
415
|
if (!result.ok || !result.local) throw new Error(result.error || 'Local OpenClaw deploy failed.')
|
|
272
416
|
setLocalStatus(result.local)
|
|
273
417
|
if (result.token) setDeployToken(result.token)
|
|
274
|
-
await
|
|
418
|
+
const verify = await api<DeployActionResponse>('POST', '/openclaw/deploy', {
|
|
419
|
+
action: 'verify',
|
|
420
|
+
endpoint: result.local.endpoint,
|
|
421
|
+
token: result.token || deployToken || undefined,
|
|
422
|
+
}).catch(() => ({ ok: false } as DeployActionResponse))
|
|
423
|
+
if (verify.verify) {
|
|
424
|
+
setVerifySummary(verify.verify.ok
|
|
425
|
+
? `Verified ${verify.verify.endpoint} with ${verify.verify.models.length} model${verify.verify.models.length === 1 ? '' : 's'}.`
|
|
426
|
+
: (verify.verify.error || verify.verify.hint || 'Verification failed.'))
|
|
427
|
+
}
|
|
428
|
+
await applyDeploymentPatch({
|
|
275
429
|
endpoint: result.local.endpoint,
|
|
276
430
|
token: result.token || deployToken,
|
|
277
431
|
name: suggestedName || `Local OpenClaw ${result.local.port}`,
|
|
278
432
|
notes: 'Managed by SwarmClaw local deploy.',
|
|
279
|
-
|
|
433
|
+
deployment: {
|
|
434
|
+
method: 'local',
|
|
435
|
+
provider: 'local',
|
|
436
|
+
useCase: 'local-dev',
|
|
437
|
+
exposure: 'private-lan',
|
|
438
|
+
lastDeployAt: Date.now(),
|
|
439
|
+
lastDeployAction: 'start-local',
|
|
440
|
+
lastDeploySummary: 'Managed local OpenClaw runtime started from SwarmClaw.',
|
|
441
|
+
lastVerifiedAt: verify.verify ? Date.now() : null,
|
|
442
|
+
lastVerifiedOk: verify.verify?.ok ?? null,
|
|
443
|
+
lastVerifiedMessage: verify.verify
|
|
444
|
+
? (verify.verify.error || verify.verify.hint || 'Verified successfully.')
|
|
445
|
+
: null,
|
|
446
|
+
},
|
|
447
|
+
})
|
|
280
448
|
showMessage('Local OpenClaw started and applied to this connection.')
|
|
281
449
|
} catch (err: unknown) {
|
|
282
450
|
setError(err instanceof Error ? err.message : 'Local OpenClaw deploy failed.')
|
|
@@ -300,6 +468,41 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
300
468
|
}
|
|
301
469
|
}
|
|
302
470
|
|
|
471
|
+
const handleRestartLocal = async () => {
|
|
472
|
+
setLoading('restarting-local')
|
|
473
|
+
setError('')
|
|
474
|
+
try {
|
|
475
|
+
const result = await api<DeployActionResponse>('POST', '/openclaw/deploy', {
|
|
476
|
+
action: 'restart-local',
|
|
477
|
+
port: localPort,
|
|
478
|
+
token: deployToken.trim() || undefined,
|
|
479
|
+
})
|
|
480
|
+
if (!result.ok || !result.local) throw new Error(result.error || 'Failed to restart local OpenClaw.')
|
|
481
|
+
setLocalStatus(result.local)
|
|
482
|
+
if (result.token) setDeployToken(result.token)
|
|
483
|
+
await applyDeploymentPatch({
|
|
484
|
+
endpoint: result.local.endpoint,
|
|
485
|
+
token: result.token || deployToken,
|
|
486
|
+
name: suggestedName || `Local OpenClaw ${result.local.port}`,
|
|
487
|
+
notes: 'Managed by SwarmClaw local deploy.',
|
|
488
|
+
deployment: {
|
|
489
|
+
method: 'local',
|
|
490
|
+
provider: 'local',
|
|
491
|
+
useCase: 'local-dev',
|
|
492
|
+
exposure: 'private-lan',
|
|
493
|
+
lastDeployAt: Date.now(),
|
|
494
|
+
lastDeployAction: 'restart-local',
|
|
495
|
+
lastDeploySummary: 'Managed local OpenClaw runtime restarted from SwarmClaw.',
|
|
496
|
+
},
|
|
497
|
+
})
|
|
498
|
+
showMessage('Restarted managed local OpenClaw runtime.')
|
|
499
|
+
} catch (err: unknown) {
|
|
500
|
+
setError(err instanceof Error ? err.message : 'Failed to restart local OpenClaw.')
|
|
501
|
+
} finally {
|
|
502
|
+
setLoading('idle')
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
303
506
|
const handleGenerateBundle = async () => {
|
|
304
507
|
setLoading('generating-bundle')
|
|
305
508
|
setError('')
|
|
@@ -311,17 +514,26 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
311
514
|
scheme: remoteScheme,
|
|
312
515
|
token: deployToken.trim() || undefined,
|
|
313
516
|
provider: remoteProvider,
|
|
517
|
+
useCase,
|
|
518
|
+
exposure,
|
|
314
519
|
})
|
|
315
520
|
if (!result.ok || !result.bundle) throw new Error(result.error || 'Failed to generate OpenClaw deploy bundle.')
|
|
316
521
|
setBundle(result.bundle)
|
|
317
522
|
setBundleFile(result.bundle.files[0]?.name || '')
|
|
318
523
|
setDeployToken(result.bundle.token)
|
|
319
|
-
await
|
|
524
|
+
await applyDeploymentPatch({
|
|
320
525
|
endpoint: result.bundle.endpoint,
|
|
321
526
|
token: result.bundle.token,
|
|
322
527
|
name: suggestedName || result.bundle.title,
|
|
323
528
|
notes: `OpenClaw remote deploy template: ${result.bundle.title}`,
|
|
324
|
-
|
|
529
|
+
deployment: buildRemoteDeploymentPatch({
|
|
530
|
+
method: 'bundle',
|
|
531
|
+
provider: remoteTemplate === 'docker' ? remoteProvider : remoteTemplate,
|
|
532
|
+
remoteTarget: remoteTemplate,
|
|
533
|
+
lastDeployAction: 'bundle',
|
|
534
|
+
lastDeploySummary: `Generated ${result.bundle.title} from SwarmClaw.`,
|
|
535
|
+
}),
|
|
536
|
+
})
|
|
325
537
|
showMessage('Remote bundle generated and applied to this connection.')
|
|
326
538
|
} catch (err: unknown) {
|
|
327
539
|
setError(err instanceof Error ? err.message : 'Failed to generate OpenClaw deploy bundle.')
|
|
@@ -330,6 +542,140 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
330
542
|
}
|
|
331
543
|
}
|
|
332
544
|
|
|
545
|
+
const handleVerify = async (overrideEndpoint?: string | null, overrideToken?: string | null) => {
|
|
546
|
+
setLoading('verifying')
|
|
547
|
+
setError('')
|
|
548
|
+
try {
|
|
549
|
+
const endpointToVerify = (overrideEndpoint || bundle?.endpoint || endpoint || '').trim()
|
|
550
|
+
const tokenToVerify = (overrideToken || deployToken || '').trim()
|
|
551
|
+
if (!endpointToVerify) throw new Error('Set an OpenClaw endpoint before verifying.')
|
|
552
|
+
const result = await api<DeployActionResponse>('POST', '/openclaw/deploy', {
|
|
553
|
+
action: 'verify',
|
|
554
|
+
endpoint: endpointToVerify,
|
|
555
|
+
token: tokenToVerify || undefined,
|
|
556
|
+
})
|
|
557
|
+
if (!result.verify) throw new Error(result.error || 'Verification failed.')
|
|
558
|
+
const summary = result.verify.ok
|
|
559
|
+
? `Verified ${result.verify.endpoint} with ${result.verify.models.length} model${result.verify.models.length === 1 ? '' : 's'}.`
|
|
560
|
+
: (result.verify.error || result.verify.hint || 'Verification failed.')
|
|
561
|
+
setVerifySummary(summary)
|
|
562
|
+
await applyDeploymentPatch({
|
|
563
|
+
endpoint: result.verify.endpoint,
|
|
564
|
+
token: tokenToVerify || undefined,
|
|
565
|
+
deployment: buildRemoteDeploymentPatch({
|
|
566
|
+
method: deployment?.method || (isLocalEndpoint(result.verify.endpoint) ? 'local' : 'bundle'),
|
|
567
|
+
lastVerifiedAt: Date.now(),
|
|
568
|
+
lastVerifiedOk: result.verify.ok,
|
|
569
|
+
lastVerifiedMessage: summary,
|
|
570
|
+
}),
|
|
571
|
+
})
|
|
572
|
+
showMessage(result.verify.ok ? 'OpenClaw verification passed.' : summary)
|
|
573
|
+
} catch (err: unknown) {
|
|
574
|
+
setError(err instanceof Error ? err.message : 'Verification failed.')
|
|
575
|
+
} finally {
|
|
576
|
+
setLoading('idle')
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const handleSshDeploy = async () => {
|
|
581
|
+
setLoading('ssh-deploy')
|
|
582
|
+
setError('')
|
|
583
|
+
setVerifySummary('')
|
|
584
|
+
try {
|
|
585
|
+
const result = await api<DeployActionResponse>('POST', '/openclaw/deploy', {
|
|
586
|
+
action: 'ssh-deploy',
|
|
587
|
+
template: remoteTemplate,
|
|
588
|
+
target: remoteTarget.trim(),
|
|
589
|
+
scheme: remoteScheme,
|
|
590
|
+
token: deployToken.trim() || undefined,
|
|
591
|
+
provider: remoteProvider,
|
|
592
|
+
useCase,
|
|
593
|
+
exposure,
|
|
594
|
+
ssh: {
|
|
595
|
+
host: sshHost.trim(),
|
|
596
|
+
user: sshUser.trim() || undefined,
|
|
597
|
+
port: sshPort,
|
|
598
|
+
keyPath: sshKeyPath.trim() || undefined,
|
|
599
|
+
targetDir: sshTargetDir.trim() || undefined,
|
|
600
|
+
},
|
|
601
|
+
})
|
|
602
|
+
if (!result.ok) throw new Error(result.error || 'Failed to start SSH deploy.')
|
|
603
|
+
if (result.bundle) {
|
|
604
|
+
setBundle(result.bundle)
|
|
605
|
+
setBundleFile(result.bundle.files[0]?.name || '')
|
|
606
|
+
}
|
|
607
|
+
if (result.token) setDeployToken(result.token)
|
|
608
|
+
setRemoteStatus(result.remote || null)
|
|
609
|
+
setCommandPreview(result.commandPreview || '')
|
|
610
|
+
await applyDeploymentPatch({
|
|
611
|
+
endpoint: result.bundle?.endpoint || endpoint || undefined,
|
|
612
|
+
token: result.token || deployToken,
|
|
613
|
+
name: suggestedName || result.bundle?.title || `SSH OpenClaw ${sshHost.trim()}`,
|
|
614
|
+
notes: `Official OpenClaw deployed over SSH to ${sshHost.trim()}.`,
|
|
615
|
+
deployment: buildRemoteDeploymentPatch({
|
|
616
|
+
method: 'ssh',
|
|
617
|
+
provider: remoteProvider,
|
|
618
|
+
lastDeployAt: Date.now(),
|
|
619
|
+
lastDeployAction: 'ssh-deploy',
|
|
620
|
+
lastDeploySummary: result.summary || `Started SSH deploy to ${sshHost.trim()}.`,
|
|
621
|
+
lastDeployProcessId: result.processId || null,
|
|
622
|
+
}),
|
|
623
|
+
})
|
|
624
|
+
showMessage(result.summary || 'Started SSH deploy.')
|
|
625
|
+
} catch (err: unknown) {
|
|
626
|
+
setError(err instanceof Error ? err.message : 'Failed to start SSH deploy.')
|
|
627
|
+
} finally {
|
|
628
|
+
setLoading('idle')
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const handleRemoteLifecycle = async (
|
|
633
|
+
action: 'remote-start' | 'remote-stop' | 'remote-restart' | 'remote-upgrade' | 'remote-backup' | 'remote-restore' | 'remote-rotate-token',
|
|
634
|
+
) => {
|
|
635
|
+
setLoading('remote-action')
|
|
636
|
+
setError('')
|
|
637
|
+
try {
|
|
638
|
+
const result = await api<DeployActionResponse>('POST', '/openclaw/deploy', {
|
|
639
|
+
action,
|
|
640
|
+
token: action === 'remote-rotate-token' ? (deployToken.trim() || undefined) : undefined,
|
|
641
|
+
backupPath: action === 'remote-restore' ? (restoreBackupPath.trim() || undefined) : undefined,
|
|
642
|
+
ssh: {
|
|
643
|
+
host: sshHost.trim(),
|
|
644
|
+
user: sshUser.trim() || undefined,
|
|
645
|
+
port: sshPort,
|
|
646
|
+
keyPath: sshKeyPath.trim() || undefined,
|
|
647
|
+
targetDir: sshTargetDir.trim() || undefined,
|
|
648
|
+
},
|
|
649
|
+
})
|
|
650
|
+
if (!result.ok) throw new Error(result.error || 'Remote lifecycle action failed.')
|
|
651
|
+
if (result.token) setDeployToken(result.token)
|
|
652
|
+
setRemoteStatus(result.remote || null)
|
|
653
|
+
setCommandPreview(result.commandPreview || '')
|
|
654
|
+
if (result.remote?.lastBackupPath) {
|
|
655
|
+
setRestoreBackupPath(result.remote.lastBackupPath)
|
|
656
|
+
}
|
|
657
|
+
await applyDeploymentPatch({
|
|
658
|
+
token: result.token || undefined,
|
|
659
|
+
deployment: buildRemoteDeploymentPatch({
|
|
660
|
+
method: 'ssh',
|
|
661
|
+
provider: remoteProvider,
|
|
662
|
+
lastDeployAt: Date.now(),
|
|
663
|
+
lastDeployAction: action,
|
|
664
|
+
lastDeploySummary: result.summary || action,
|
|
665
|
+
lastDeployProcessId: result.processId || null,
|
|
666
|
+
lastBackupPath: action === 'remote-backup' || action === 'remote-restore'
|
|
667
|
+
? (result.remote?.lastBackupPath || restoreBackupPath.trim() || null)
|
|
668
|
+
: undefined,
|
|
669
|
+
}),
|
|
670
|
+
})
|
|
671
|
+
showMessage(result.summary || 'Remote lifecycle action started.')
|
|
672
|
+
} catch (err: unknown) {
|
|
673
|
+
setError(err instanceof Error ? err.message : 'Remote lifecycle action failed.')
|
|
674
|
+
} finally {
|
|
675
|
+
setLoading('idle')
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
333
679
|
return (
|
|
334
680
|
<div className={`rounded-[16px] border border-white/[0.08] bg-surface ${compact ? 'p-4' : 'p-5'} text-left`}>
|
|
335
681
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
@@ -405,6 +751,26 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
405
751
|
>
|
|
406
752
|
{loading === 'starting-local' ? 'Starting…' : 'Deploy on This Host'}
|
|
407
753
|
</button>
|
|
754
|
+
{localStatus?.running && (
|
|
755
|
+
<button
|
|
756
|
+
type="button"
|
|
757
|
+
onClick={handleRestartLocal}
|
|
758
|
+
disabled={loading !== 'idle'}
|
|
759
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3.5 py-2 text-[12px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
760
|
+
>
|
|
761
|
+
{loading === 'restarting-local' ? 'Restarting…' : 'Restart'}
|
|
762
|
+
</button>
|
|
763
|
+
)}
|
|
764
|
+
{localStatus?.running && (
|
|
765
|
+
<button
|
|
766
|
+
type="button"
|
|
767
|
+
onClick={() => void handleVerify(localStatus.endpoint, deployToken || localStatus.token)}
|
|
768
|
+
disabled={loading !== 'idle'}
|
|
769
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3.5 py-2 text-[12px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
770
|
+
>
|
|
771
|
+
{loading === 'verifying' ? 'Verifying…' : 'Verify'}
|
|
772
|
+
</button>
|
|
773
|
+
)}
|
|
408
774
|
{localStatus?.running && (
|
|
409
775
|
<button
|
|
410
776
|
type="button"
|
|
@@ -453,6 +819,12 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
453
819
|
For a durable OS service, use the generated install command after the quick deploy works.
|
|
454
820
|
</div>
|
|
455
821
|
</div>
|
|
822
|
+
{verifySummary && (
|
|
823
|
+
<div className="rounded-[10px] border border-white/[0.05] bg-white/[0.02] px-3 py-2 md:col-span-2">
|
|
824
|
+
<div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60">Verification</div>
|
|
825
|
+
<div className="mt-1 text-[12px] text-text-2 leading-relaxed">{verifySummary}</div>
|
|
826
|
+
</div>
|
|
827
|
+
)}
|
|
456
828
|
</div>
|
|
457
829
|
)}
|
|
458
830
|
|
|
@@ -509,7 +881,7 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
509
881
|
</div>
|
|
510
882
|
|
|
511
883
|
{remoteTemplate === 'docker' && (
|
|
512
|
-
<div>
|
|
884
|
+
<div className="space-y-4">
|
|
513
885
|
<div className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/70 mb-2">VPS provider</div>
|
|
514
886
|
<div className="grid gap-2 md:grid-cols-3 xl:grid-cols-5">
|
|
515
887
|
{PROVIDER_OPTIONS.map((option) => (
|
|
@@ -527,6 +899,87 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
527
899
|
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
528
900
|
SwarmClaw generates a provider-specific runbook plus a cloud-init quickstart, but the runtime itself still comes from the official OpenClaw Docker image.
|
|
529
901
|
</p>
|
|
902
|
+
|
|
903
|
+
<div>
|
|
904
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/70 mb-2">Use case preset</div>
|
|
905
|
+
<div className="grid gap-2 md:grid-cols-2 xl:grid-cols-5">
|
|
906
|
+
{USE_CASE_OPTIONS.map((option) => (
|
|
907
|
+
<button
|
|
908
|
+
key={option.id}
|
|
909
|
+
type="button"
|
|
910
|
+
onClick={() => setUseCase(option.id)}
|
|
911
|
+
className={`rounded-[12px] border px-3 py-3 text-left transition-all cursor-pointer ${badgeTone(useCase === option.id)}`}
|
|
912
|
+
>
|
|
913
|
+
<div className="text-[13px] font-700">{option.label}</div>
|
|
914
|
+
<div className="mt-1 text-[11px] leading-relaxed text-text-3">{option.detail}</div>
|
|
915
|
+
</button>
|
|
916
|
+
))}
|
|
917
|
+
</div>
|
|
918
|
+
</div>
|
|
919
|
+
|
|
920
|
+
<div>
|
|
921
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/70 mb-2">Safe exposure preset</div>
|
|
922
|
+
<div className="grid gap-2 md:grid-cols-2 xl:grid-cols-5">
|
|
923
|
+
{EXPOSURE_OPTIONS.map((option) => (
|
|
924
|
+
<button
|
|
925
|
+
key={option.id}
|
|
926
|
+
type="button"
|
|
927
|
+
onClick={() => setExposure(option.id)}
|
|
928
|
+
className={`rounded-[12px] border px-3 py-3 text-left transition-all cursor-pointer ${badgeTone(exposure === option.id)}`}
|
|
929
|
+
>
|
|
930
|
+
<div className="text-[13px] font-700">{option.label}</div>
|
|
931
|
+
<div className="mt-1 text-[11px] leading-relaxed text-text-3">{option.detail}</div>
|
|
932
|
+
</button>
|
|
933
|
+
))}
|
|
934
|
+
</div>
|
|
935
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
936
|
+
Smart Deploy keeps the OpenClaw runtime official-only and generates the surrounding exposure config in-house so operators do not need third-party deploy services.
|
|
937
|
+
</p>
|
|
938
|
+
</div>
|
|
939
|
+
|
|
940
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-white/[0.02] p-4">
|
|
941
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/70 mb-3">In-House SSH Deploy</div>
|
|
942
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
943
|
+
<input
|
|
944
|
+
type="text"
|
|
945
|
+
value={sshHost}
|
|
946
|
+
onChange={(e) => setSshHost(e.target.value)}
|
|
947
|
+
placeholder="gateway.your-vps.com"
|
|
948
|
+
className="w-full rounded-[12px] border border-white/[0.08] bg-bg px-3 py-3 text-[13px] text-text font-mono outline-none focus:border-accent-bright/30"
|
|
949
|
+
/>
|
|
950
|
+
<input
|
|
951
|
+
type="text"
|
|
952
|
+
value={sshUser}
|
|
953
|
+
onChange={(e) => setSshUser(e.target.value)}
|
|
954
|
+
placeholder="root"
|
|
955
|
+
className="w-full rounded-[12px] border border-white/[0.08] bg-bg px-3 py-3 text-[13px] text-text font-mono outline-none focus:border-accent-bright/30"
|
|
956
|
+
/>
|
|
957
|
+
<input
|
|
958
|
+
type="number"
|
|
959
|
+
value={sshPort}
|
|
960
|
+
onChange={(e) => setSshPort(Number.parseInt(e.target.value, 10) || 22)}
|
|
961
|
+
placeholder="22"
|
|
962
|
+
className="w-full rounded-[12px] border border-white/[0.08] bg-bg px-3 py-3 text-[13px] text-text font-mono outline-none focus:border-accent-bright/30"
|
|
963
|
+
/>
|
|
964
|
+
<input
|
|
965
|
+
type="text"
|
|
966
|
+
value={sshKeyPath}
|
|
967
|
+
onChange={(e) => setSshKeyPath(e.target.value)}
|
|
968
|
+
placeholder="~/.ssh/id_ed25519"
|
|
969
|
+
className="w-full rounded-[12px] border border-white/[0.08] bg-bg px-3 py-3 text-[13px] text-text font-mono outline-none focus:border-accent-bright/30"
|
|
970
|
+
/>
|
|
971
|
+
</div>
|
|
972
|
+
<input
|
|
973
|
+
type="text"
|
|
974
|
+
value={sshTargetDir}
|
|
975
|
+
onChange={(e) => setSshTargetDir(e.target.value)}
|
|
976
|
+
placeholder="/opt/openclaw"
|
|
977
|
+
className="mt-3 w-full rounded-[12px] border border-white/[0.08] bg-bg px-3 py-3 text-[13px] text-text font-mono outline-none focus:border-accent-bright/30"
|
|
978
|
+
/>
|
|
979
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
980
|
+
SwarmClaw will push the generated official-image bundle to this host over SSH and run the bootstrap there. This stays inside your own infra and does not rely on outside OpenClaw deployers.
|
|
981
|
+
</p>
|
|
982
|
+
</div>
|
|
530
983
|
</div>
|
|
531
984
|
)}
|
|
532
985
|
|
|
@@ -539,6 +992,24 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
539
992
|
>
|
|
540
993
|
{loading === 'generating-bundle' ? 'Generating…' : 'Generate Bundle'}
|
|
541
994
|
</button>
|
|
995
|
+
{remoteTemplate === 'docker' && (
|
|
996
|
+
<button
|
|
997
|
+
type="button"
|
|
998
|
+
onClick={handleSshDeploy}
|
|
999
|
+
disabled={loading !== 'idle' || !sshHost.trim()}
|
|
1000
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3.5 py-2 text-[12px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
1001
|
+
>
|
|
1002
|
+
{loading === 'ssh-deploy' ? 'Deploying…' : 'Deploy Over SSH'}
|
|
1003
|
+
</button>
|
|
1004
|
+
)}
|
|
1005
|
+
<button
|
|
1006
|
+
type="button"
|
|
1007
|
+
onClick={() => void handleVerify(bundle?.endpoint || endpoint || remoteTarget, deployToken)}
|
|
1008
|
+
disabled={loading !== 'idle' || (!bundle?.endpoint && !endpoint && !remoteTarget.trim())}
|
|
1009
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3.5 py-2 text-[12px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
1010
|
+
>
|
|
1011
|
+
{loading === 'verifying' ? 'Verifying…' : 'Verify Endpoint'}
|
|
1012
|
+
</button>
|
|
542
1013
|
{bundle && (
|
|
543
1014
|
<button
|
|
544
1015
|
type="button"
|
|
@@ -550,6 +1021,117 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
550
1021
|
)}
|
|
551
1022
|
</div>
|
|
552
1023
|
|
|
1024
|
+
{remoteTemplate === 'docker' && sshHost.trim() && (
|
|
1025
|
+
<div className="space-y-3">
|
|
1026
|
+
<div className="flex flex-wrap gap-2">
|
|
1027
|
+
<button
|
|
1028
|
+
type="button"
|
|
1029
|
+
onClick={() => void handleRemoteLifecycle('remote-start')}
|
|
1030
|
+
disabled={loading !== 'idle'}
|
|
1031
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3 py-1.5 text-[11px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
1032
|
+
>
|
|
1033
|
+
Start
|
|
1034
|
+
</button>
|
|
1035
|
+
<button
|
|
1036
|
+
type="button"
|
|
1037
|
+
onClick={() => void handleRemoteLifecycle('remote-restart')}
|
|
1038
|
+
disabled={loading !== 'idle'}
|
|
1039
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3 py-1.5 text-[11px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
1040
|
+
>
|
|
1041
|
+
Restart
|
|
1042
|
+
</button>
|
|
1043
|
+
<button
|
|
1044
|
+
type="button"
|
|
1045
|
+
onClick={() => void handleRemoteLifecycle('remote-upgrade')}
|
|
1046
|
+
disabled={loading !== 'idle'}
|
|
1047
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3 py-1.5 text-[11px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
1048
|
+
>
|
|
1049
|
+
Upgrade
|
|
1050
|
+
</button>
|
|
1051
|
+
<button
|
|
1052
|
+
type="button"
|
|
1053
|
+
onClick={() => void handleRemoteLifecycle('remote-backup')}
|
|
1054
|
+
disabled={loading !== 'idle'}
|
|
1055
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3 py-1.5 text-[11px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
1056
|
+
>
|
|
1057
|
+
Backup
|
|
1058
|
+
</button>
|
|
1059
|
+
<button
|
|
1060
|
+
type="button"
|
|
1061
|
+
onClick={() => void handleRemoteLifecycle('remote-rotate-token')}
|
|
1062
|
+
disabled={loading !== 'idle'}
|
|
1063
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3 py-1.5 text-[11px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
1064
|
+
>
|
|
1065
|
+
Rotate token
|
|
1066
|
+
</button>
|
|
1067
|
+
<button
|
|
1068
|
+
type="button"
|
|
1069
|
+
onClick={() => void handleRemoteLifecycle('remote-stop')}
|
|
1070
|
+
disabled={loading !== 'idle'}
|
|
1071
|
+
className="rounded-[10px] border border-red-400/20 bg-red-400/[0.06] px-3 py-1.5 text-[11px] font-700 text-red-300 cursor-pointer hover:bg-red-400/[0.1] transition-all disabled:opacity-40"
|
|
1072
|
+
>
|
|
1073
|
+
Stop
|
|
1074
|
+
</button>
|
|
1075
|
+
</div>
|
|
1076
|
+
<div className="grid gap-2 md:grid-cols-[1fr_auto]">
|
|
1077
|
+
<input
|
|
1078
|
+
type="text"
|
|
1079
|
+
value={restoreBackupPath}
|
|
1080
|
+
onChange={(e) => setRestoreBackupPath(e.target.value)}
|
|
1081
|
+
placeholder="/opt/openclaw/backups/openclaw-backup-123456789.tgz"
|
|
1082
|
+
className="w-full rounded-[12px] border border-white/[0.08] bg-bg px-3 py-3 text-[13px] text-text font-mono outline-none focus:border-accent-bright/30"
|
|
1083
|
+
/>
|
|
1084
|
+
<button
|
|
1085
|
+
type="button"
|
|
1086
|
+
onClick={() => void handleRemoteLifecycle('remote-restore')}
|
|
1087
|
+
disabled={loading !== 'idle' || !restoreBackupPath.trim()}
|
|
1088
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3 py-1.5 text-[11px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
1089
|
+
>
|
|
1090
|
+
Restore backup
|
|
1091
|
+
</button>
|
|
1092
|
+
</div>
|
|
1093
|
+
</div>
|
|
1094
|
+
)}
|
|
1095
|
+
|
|
1096
|
+
{(verifySummary || commandPreview || remoteStatus) && (
|
|
1097
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-4">
|
|
1098
|
+
{verifySummary && (
|
|
1099
|
+
<div className="text-[12px] text-text-2 leading-relaxed">{verifySummary}</div>
|
|
1100
|
+
)}
|
|
1101
|
+
{remoteStatus && (
|
|
1102
|
+
<div className="mt-3 grid gap-3 md:grid-cols-3">
|
|
1103
|
+
<div className="rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2">
|
|
1104
|
+
<div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60">Remote action</div>
|
|
1105
|
+
<div className="mt-1 text-[12px] text-text-2">{remoteStatus.action || remoteStatus.lastSummary || 'Idle'}</div>
|
|
1106
|
+
</div>
|
|
1107
|
+
<div className="rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2">
|
|
1108
|
+
<div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60">Target</div>
|
|
1109
|
+
<div className="mt-1 text-[12px] text-text-2 font-mono break-all">{remoteStatus.target || sshHost || 'n/a'}</div>
|
|
1110
|
+
</div>
|
|
1111
|
+
<div className="rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2">
|
|
1112
|
+
<div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60">Status</div>
|
|
1113
|
+
<div className="mt-1 text-[12px] text-text-2">{remoteStatus.status}</div>
|
|
1114
|
+
</div>
|
|
1115
|
+
</div>
|
|
1116
|
+
)}
|
|
1117
|
+
{(commandPreview || remoteStatus?.lastCommandPreview) && (
|
|
1118
|
+
<pre className="mt-3 overflow-x-auto rounded-[10px] border border-white/[0.05] bg-black/20 px-3 py-3 text-[11px] text-text-2/80 whitespace-pre-wrap">
|
|
1119
|
+
{commandPreview || remoteStatus?.lastCommandPreview}
|
|
1120
|
+
</pre>
|
|
1121
|
+
)}
|
|
1122
|
+
{!!remoteStatus?.tail && (
|
|
1123
|
+
<pre className="mt-3 overflow-x-auto rounded-[10px] border border-white/[0.05] bg-black/20 px-3 py-3 text-[11px] text-text-2/80 whitespace-pre-wrap">
|
|
1124
|
+
{remoteStatus.tail}
|
|
1125
|
+
</pre>
|
|
1126
|
+
)}
|
|
1127
|
+
{remoteStatus?.lastBackupPath && (
|
|
1128
|
+
<div className="mt-3 text-[12px] text-text-3">
|
|
1129
|
+
Last backup path: <code className="text-text-2">{remoteStatus.lastBackupPath}</code>
|
|
1130
|
+
</div>
|
|
1131
|
+
)}
|
|
1132
|
+
</div>
|
|
1133
|
+
)}
|
|
1134
|
+
|
|
553
1135
|
{bundle && (
|
|
554
1136
|
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-4">
|
|
555
1137
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
@@ -612,13 +1194,13 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
612
1194
|
</div>
|
|
613
1195
|
)}
|
|
614
1196
|
|
|
615
|
-
{(message || error || localStatus?.lastError) && (
|
|
1197
|
+
{(message || error || localStatus?.lastError || remoteStatus?.lastError) && (
|
|
616
1198
|
<div className={`mt-4 rounded-[12px] border px-3 py-2 text-[12px] ${
|
|
617
|
-
error || localStatus?.lastError
|
|
1199
|
+
error || localStatus?.lastError || remoteStatus?.lastError
|
|
618
1200
|
? 'border-red-400/20 bg-red-400/[0.06] text-red-200'
|
|
619
1201
|
: 'border-emerald-500/20 bg-emerald-500/[0.06] text-emerald-200'
|
|
620
1202
|
}`}>
|
|
621
|
-
{error || localStatus?.lastError || message}
|
|
1203
|
+
{error || localStatus?.lastError || remoteStatus?.lastError || message}
|
|
622
1204
|
</div>
|
|
623
1205
|
)}
|
|
624
1206
|
</div>
|