diff --git a/cmd/web-ui/static/app.js b/cmd/web-ui/static/app.js index bc213bb..c62c918 100644 --- a/cmd/web-ui/static/app.js +++ b/cmd/web-ui/static/app.js @@ -48,6 +48,7 @@ let sessionsUnsubscribe = null; let openclawState = { instances: {} }; let openclawUnsubscribe = null; + let infraUnsubscribe = null; let swarmState = { services: {} }; // keyed by service name let agentsState = createAgentsState(); let agentsUnsubscribe = null; @@ -111,6 +112,10 @@ openclawUnsubscribe(); openclawUnsubscribe = null; } + if (infraUnsubscribe) { + infraUnsubscribe(); + infraUnsubscribe = null; + } if (agentsUnsubscribe) { agentsUnsubscribe(); agentsUnsubscribe = null; @@ -151,8 +156,8 @@ renderSessions(); } else if (path.startsWith('/agents')) { renderAgents(); - } else if (path.startsWith('/openclaw')) { - renderOpenClaw(); + } else if (path.startsWith('/infrastructure')) { + renderInfrastructure(); } else if (path.startsWith('/sessions/')) { renderSession(path.split('/sessions/')[1]); } else if (path.startsWith('/runs/')) { @@ -662,40 +667,54 @@ }); } - async function renderOpenClaw() { - app.innerHTML = '

Loading...

'; + async function renderInfrastructure() { + app.innerHTML = '

Loading...

'; - openclawUnsubscribe = subscribeWS(handleOpenClawWS); + infraUnsubscribe = subscribeWS(handleInfraWS); try { - const data = await api('/v1/events?event_type=openclaw.snapshot&limit=100'); - mergeOpenClawEvents(data.events || []); - if (isCurrentPath('/openclaw')) { - renderOpenClawGrid(); + const [ocData, swarmData] = await Promise.all([ + api('/v1/events?event_type=openclaw.snapshot&limit=100'), + api('/v1/events?event_type=swarm.snapshot&limit=10').catch(() => ({ events: [] })), + ]); + + mergeOpenClawEvents(ocData.events || []); + for (const evt of swarmData.events || []) mergeSwarmSnapshot(evt); + + if (isCurrentPath('/infrastructure')) { + renderInfraGrid(); } } catch (e) { - if (isCurrentPath('/openclaw')) { - app.innerHTML = `

Error loading: ${escapeHTML(e.message)}

`; + if (isCurrentPath('/infrastructure')) { + app.innerHTML = `

Error: ${escapeHTML(e.message)}

`; } } } - function handleOpenClawWS(msg) { - if (msg.type !== 'message') { + function handleInfraWS(msg) { + if (msg.type !== 'message') return; + + const eventType = getEnvelopeType(msg.data); + + if (eventType === 'openclaw.snapshot') { + mergeOpenClawEvents([msg.data]); + if (isCurrentPath('/infrastructure')) renderInfraGrid(); + if (isCurrentPath('/agents')) renderAgentVMStrip(); return; } - if (getEnvelopeType(msg.data) !== 'openclaw.snapshot') { + if (eventType === 'swarm.snapshot') { + mergeSwarmSnapshot(msg.data); + if (isCurrentPath('/infrastructure')) renderInfraGrid(); + renderSwarmStrip_dash(); return; } - mergeOpenClawEvents([msg.data]); - - if (isCurrentPath('/openclaw')) { - renderOpenClawGrid(); - } - if (isCurrentPath('/agents')) { - renderAgentVMStrip(); + if (eventType === 'swarm.service.snapshot') { + mergeSwarmServiceSnapshot(msg.data); + if (isCurrentPath('/infrastructure')) renderInfraGrid(); + renderSwarmStrip_dash(); + return; } } @@ -730,71 +749,231 @@ if (svc && svc.name) swarmState.services[svc.name] = svc; } - function renderOpenClawGrid() { - const names = Object.keys(openclawState.instances).sort(); - - if (names.length === 0) { - app.innerHTML = ` - -

No OpenClaw instances found

- `; - return; - } + function renderInfraGrid() { + const vmNames = Object.keys(openclawState.instances).sort(); + const services = Object.values(swarmState.services); app.innerHTML = ` -
- ${names.map(name => { - const evt = openclawState.instances[name]; - const payload = getEnvelopePayload(evt); - const inst = payload.instance || {}; - const host = payload.host || {}; - const guest = payload.guest; - const issues = payload.issues; - return ` -
-
-

${escapeHTML(inst.name || name)}

-
- ${host.state === 'running' ? 'Running' : 'Stopped'} -
-
-
Updated ${escapeHTML(relativeTime(getEnvelopeTS(evt)))}
- - - - - - - -
Host${escapeHTML(inst.host || '-')}
Domain${escapeHTML(inst.domain || '-')}
vCPUs${host.vcpus || '-'}
Memory${escapeHTML(formatBytes(host.memory_kib ? host.memory_kib * 1024 : 0) || '-')}
Disk${escapeHTML(formatBytes(host.disk_actual_bytes) || '-')}
Autostart${host.autostart ? 'Yes' : 'No'}
- ${guest ? ` -
- - - - - - - - -
Gateway${guest.service_active ? 'Active' : 'Inactive'}
HTTP${guest.http_status || 'N/A'}
Version${escapeHTML(guest.version || '-')}
Guest Mem${guest.memory_percent !== undefined ? guest.memory_percent.toFixed(1) : '-'}%
Guest Disk${guest.disk_percent !== undefined ? guest.disk_percent.toFixed(1) : '-'}%
Load${guest.load_average !== undefined ? guest.load_average.toFixed(2) : '-'}
Uptime${escapeHTML(guest.service_uptime || '-')}
- ` : ''} - ${issues && Object.values(issues).some(Boolean) ? ` -
-
Issues
-
- ${Object.entries(issues).filter(([, value]) => value).map(([key]) => ` - ${escapeHTML(key.replace(/_/g, ' '))} - `).join('')} -
- ` : ''} -
- `; - }).join('')} +
+

VMs

+ ${vmNames.length === 0 + ? '

No VM data

' + : `
${vmNames.map(name => renderVMCard(name)).join('')}
` + } +
+ +
+

Services

+ ${services.length === 0 + ? '

No swarm service data

' + : `
${services.map(svc => renderServiceCard(svc)).join('')}
` + } +
+ `; + } + + function renderVMCard(name) { + const evt = openclawState.instances[name]; + const payload = getEnvelopePayload(evt); + const inst = payload.instance || {}; + const host = payload.host || {}; + const guest = payload.guest; + const issues = payload.issues; + + return ` +
+
+

${escapeHTML(inst.name || name)}

+
+ ${host.state === 'running' ? 'Running' : 'Stopped'} +
+
+
Updated ${escapeHTML(relativeTime(getEnvelopeTS(evt)))}
+ + + + + + + +
Host${escapeHTML(inst.host || '-')}
Domain${escapeHTML(inst.domain || '-')}
vCPUs${host.vcpus || '-'}
Memory${escapeHTML(formatBytes(host.memory_kib ? host.memory_kib * 1024 : 0) || '-')}
Disk${escapeHTML(formatBytes(host.disk_actual_bytes) || '-')}
Autostart${host.autostart ? 'Yes' : 'No'}
+ ${guest ? ` +
+ + + + + + + + +
Gateway${guest.service_active ? 'Active' : 'Inactive'}
HTTP${guest.http_status || 'N/A'}
Version${escapeHTML(guest.version || '-')}
Guest Mem${guest.memory_percent !== undefined ? guest.memory_percent.toFixed(1) : '-'}%
Guest Disk${guest.disk_percent !== undefined ? guest.disk_percent.toFixed(1) : '-'}%
Load${guest.load_average !== undefined ? guest.load_average.toFixed(2) : '-'}
Uptime${escapeHTML(guest.service_uptime || '-')}
+ ` : ''} + ${issues && Object.values(issues).some(Boolean) ? ` +
+
Issues
+
+ ${Object.entries(issues).filter(([, value]) => value).map(([key]) => ` + ${escapeHTML(key.replace(/_/g, ' '))} + `).join('')} +
+ ` : ''} +
+ `; + } + + function renderServiceCard(svc) { + const role = svc.role || 'unknown'; + switch (role) { + case 'llm-proxy': return renderLLMProxyCard(svc); + case 'db': return renderDBCard(svc); + case 'search': return renderSearchCard(svc); + case 'mcp': return renderMCPCard(svc); + case 'voice': return renderVoiceCard(svc); + case 'automation':return renderAutomationCard(svc); + default: return renderGenericServiceCard(svc); + } + } + + function serviceCardHeader(svc) { + return ` +
+
+
${escapeHTML(svc.name)}
+
${escapeHTML(svc.role || '')}
+
+ ${escapeHTML(svc.status || 'down')} +
+ `; + } + + function serviceStatRow(label, value, valueClass) { + return ` +
+ ${escapeHTML(label)} + ${value} +
+ `; + } + + function formatUptime(sec) { + if (!sec) return '-'; + if (sec < 60) return sec + 's'; + if (sec < 3600) return Math.floor(sec / 60) + 'm'; + if (sec < 86400) return Math.floor(sec / 3600) + 'h ' + Math.floor((sec % 3600) / 60) + 'm'; + return Math.floor(sec / 86400) + 'd ' + Math.floor((sec % 86400) / 3600) + 'h'; + } + + function renderLLMProxyCard(svc) { + const extra = svc.extra || {}; + const modelCount = extra.model_count; + const cooldowns = extra.cooldown_count || 0; + const httpStatus = svc.http_status; + const httpClass = httpStatus === 200 ? 'ok' : httpStatus ? 'bad' : ''; + + return ` +
+ ${serviceCardHeader(svc)} +
+ ${modelCount !== undefined ? modelCount : '-'} + models +
+ ${cooldowns > 0 ? `
⚠ ${cooldowns} model${cooldowns > 1 ? 's' : ''} in cooldown
` : ''} +
+ ${serviceStatRow('HTTP', httpStatus ? String(httpStatus) : '-', httpClass)} + ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')} + ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')} +
+
+ `; + } + + function renderDBCard(svc) { + const healthClass = svc.health_state === 'healthy' ? 'ok' : svc.health_state === 'unhealthy' ? 'bad' : ''; + return ` +
+ ${serviceCardHeader(svc)} +
+ ${serviceStatRow('Health', escapeHTML(svc.health_state || 'none'), healthClass)} + ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')} + ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')} +
+
+ `; + } + + function renderSearchCard(svc) { + const extra = svc.extra || {}; + const ms = extra.response_ms; + const httpStatus = svc.http_status; + const httpClass = httpStatus === 200 ? 'ok' : httpStatus ? 'bad' : ''; + return ` +
+ ${serviceCardHeader(svc)} +
+ ${serviceStatRow('HTTP', httpStatus ? String(httpStatus) : '-', httpClass)} + ${ms !== undefined ? serviceStatRow('Response', ms + 'ms', ms < 500 ? 'ok' : 'warn') : ''} + ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')} +
+
+ `; + } + + function renderMCPCard(svc) { + const extra = svc.extra || {}; + const reachable = extra.port_reachable; + return ` +
+ ${serviceCardHeader(svc)} +
+ ${reachable !== undefined ? serviceStatRow('Port', reachable ? 'reachable' : 'unreachable', reachable ? 'ok' : 'bad') : ''} + ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')} + ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')} +
+
+ `; + } + + function renderVoiceCard(svc) { + const healthClass = svc.health_state === 'healthy' ? 'ok' : svc.health_state === 'unhealthy' ? 'bad' : ''; + return ` +
+ ${serviceCardHeader(svc)} +
+ ${serviceStatRow('Health', escapeHTML(svc.health_state || 'none'), healthClass)} + ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')} + ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')} +
+
+ `; + } + + function renderAutomationCard(svc) { + const healthClass = svc.health_state === 'healthy' ? 'ok' : svc.health_state === 'unhealthy' ? 'bad' : ''; + return ` +
+ ${serviceCardHeader(svc)} +
+ ${serviceStatRow('Health', escapeHTML(svc.health_state || 'none'), healthClass)} + ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')} + ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')} +
+
+ `; + } + + function renderGenericServiceCard(svc) { + return ` +
+ ${serviceCardHeader(svc)} +
+ ${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')} + ${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')} +
`; } diff --git a/cmd/web-ui/static/index.html b/cmd/web-ui/static/index.html index 4a3d17e..0ef823b 100644 --- a/cmd/web-ui/static/index.html +++ b/cmd/web-ui/static/index.html @@ -9,13 +9,15 @@ +
- + +

Loading...