feat: rename OpenClaw to Infrastructure page, add service cards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-03-18 10:20:28 -07:00
parent 93edd39a2b
commit cd2f345454
2 changed files with 263 additions and 82 deletions
+216 -37
View File
@@ -48,6 +48,7 @@
let sessionsUnsubscribe = null; let sessionsUnsubscribe = null;
let openclawState = { instances: {} }; let openclawState = { instances: {} };
let openclawUnsubscribe = null; let openclawUnsubscribe = null;
let infraUnsubscribe = null;
let swarmState = { services: {} }; // keyed by service name let swarmState = { services: {} }; // keyed by service name
let agentsState = createAgentsState(); let agentsState = createAgentsState();
let agentsUnsubscribe = null; let agentsUnsubscribe = null;
@@ -111,6 +112,10 @@
openclawUnsubscribe(); openclawUnsubscribe();
openclawUnsubscribe = null; openclawUnsubscribe = null;
} }
if (infraUnsubscribe) {
infraUnsubscribe();
infraUnsubscribe = null;
}
if (agentsUnsubscribe) { if (agentsUnsubscribe) {
agentsUnsubscribe(); agentsUnsubscribe();
agentsUnsubscribe = null; agentsUnsubscribe = null;
@@ -151,8 +156,8 @@
renderSessions(); renderSessions();
} else if (path.startsWith('/agents')) { } else if (path.startsWith('/agents')) {
renderAgents(); renderAgents();
} else if (path.startsWith('/openclaw')) { } else if (path.startsWith('/infrastructure')) {
renderOpenClaw(); renderInfrastructure();
} else if (path.startsWith('/sessions/')) { } else if (path.startsWith('/sessions/')) {
renderSession(path.split('/sessions/')[1]); renderSession(path.split('/sessions/')[1]);
} else if (path.startsWith('/runs/')) { } else if (path.startsWith('/runs/')) {
@@ -662,40 +667,54 @@
}); });
} }
async function renderOpenClaw() { async function renderInfrastructure() {
app.innerHTML = '<div class="page-header"><h2>OpenClaw</h2></div><p class="empty-state">Loading...</p>'; app.innerHTML = '<div class="page-header"><h2>Infrastructure</h2></div><p class="empty-state">Loading...</p>';
openclawUnsubscribe = subscribeWS(handleOpenClawWS); infraUnsubscribe = subscribeWS(handleInfraWS);
try { try {
const data = await api('/v1/events?event_type=openclaw.snapshot&limit=100'); const [ocData, swarmData] = await Promise.all([
mergeOpenClawEvents(data.events || []); api('/v1/events?event_type=openclaw.snapshot&limit=100'),
if (isCurrentPath('/openclaw')) { api('/v1/events?event_type=swarm.snapshot&limit=10').catch(() => ({ events: [] })),
renderOpenClawGrid(); ]);
mergeOpenClawEvents(ocData.events || []);
for (const evt of swarmData.events || []) mergeSwarmSnapshot(evt);
if (isCurrentPath('/infrastructure')) {
renderInfraGrid();
} }
} catch (e) { } catch (e) {
if (isCurrentPath('/openclaw')) { if (isCurrentPath('/infrastructure')) {
app.innerHTML = `<div class="page-header"><h2>OpenClaw</h2></div><p class="empty-state">Error loading: ${escapeHTML(e.message)}</p>`; app.innerHTML = `<div class="page-header"><h2>Infrastructure</h2></div><p class="empty-state">Error: ${escapeHTML(e.message)}</p>`;
} }
} }
} }
function handleOpenClawWS(msg) { function handleInfraWS(msg) {
if (msg.type !== 'message') { if (msg.type !== 'message') return;
return;
}
if (getEnvelopeType(msg.data) !== 'openclaw.snapshot') { const eventType = getEnvelopeType(msg.data);
return;
}
if (eventType === 'openclaw.snapshot') {
mergeOpenClawEvents([msg.data]); mergeOpenClawEvents([msg.data]);
if (isCurrentPath('/infrastructure')) renderInfraGrid();
if (isCurrentPath('/openclaw')) { if (isCurrentPath('/agents')) renderAgentVMStrip();
renderOpenClawGrid(); return;
} }
if (isCurrentPath('/agents')) {
renderAgentVMStrip(); if (eventType === 'swarm.snapshot') {
mergeSwarmSnapshot(msg.data);
if (isCurrentPath('/infrastructure')) renderInfraGrid();
renderSwarmStrip_dash();
return;
}
if (eventType === 'swarm.service.snapshot') {
mergeSwarmServiceSnapshot(msg.data);
if (isCurrentPath('/infrastructure')) renderInfraGrid();
renderSwarmStrip_dash();
return;
} }
} }
@@ -730,23 +749,34 @@
if (svc && svc.name) swarmState.services[svc.name] = svc; if (svc && svc.name) swarmState.services[svc.name] = svc;
} }
function renderOpenClawGrid() { function renderInfraGrid() {
const names = Object.keys(openclawState.instances).sort(); const vmNames = Object.keys(openclawState.instances).sort();
const services = Object.values(swarmState.services);
if (names.length === 0) {
app.innerHTML = `
<div class="page-header"><h2>OpenClaw</h2></div>
<p class="empty-state">No OpenClaw instances found</p>
`;
return;
}
app.innerHTML = ` app.innerHTML = `
<div class="page-header"> <div class="page-header">
<h2>OpenClaw <span class="live-indicator"><span class="live-dot"></span>Live</span></h2> <h2>Infrastructure <span class="live-indicator"><span class="live-dot"></span>Live</span></h2>
</div> </div>
<div class="vm-grid">
${names.map(name => { <div class="infra-section">
<p class="infra-section-title">VMs</p>
${vmNames.length === 0
? '<p class="empty-state">No VM data</p>'
: `<div class="vm-grid">${vmNames.map(name => renderVMCard(name)).join('')}</div>`
}
</div>
<div class="infra-section">
<p class="infra-section-title">Services</p>
${services.length === 0
? '<p class="empty-state">No swarm service data</p>'
: `<div class="service-grid">${services.map(svc => renderServiceCard(svc)).join('')}</div>`
}
</div>
`;
}
function renderVMCard(name) {
const evt = openclawState.instances[name]; const evt = openclawState.instances[name];
const payload = getEnvelopePayload(evt); const payload = getEnvelopePayload(evt);
const inst = payload.instance || {}; const inst = payload.instance || {};
@@ -794,7 +824,156 @@
` : ''} ` : ''}
</div> </div>
`; `;
}).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 `
<div class="service-card-header">
<div>
<div class="service-card-name">${escapeHTML(svc.name)}</div>
<div class="service-role-tag">${escapeHTML(svc.role || '')}</div>
</div>
<span class="service-badge ${escapeHTML(svc.status || 'down')}">${escapeHTML(svc.status || 'down')}</span>
</div>
`;
}
function serviceStatRow(label, value, valueClass) {
return `
<div class="service-stat-row">
<span class="service-stat-label">${escapeHTML(label)}</span>
<span class="service-stat-value${valueClass ? ' ' + valueClass : ''}">${value}</span>
</div>
`;
}
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 `
<div class="service-card">
${serviceCardHeader(svc)}
<div style="display:flex;align-items:baseline;gap:0.5rem">
<span class="llm-model-count">${modelCount !== undefined ? modelCount : '-'}</span>
<span class="llm-model-label">models</span>
</div>
${cooldowns > 0 ? `<div class="llm-cooldown-banner">⚠ ${cooldowns} model${cooldowns > 1 ? 's' : ''} in cooldown</div>` : ''}
<div class="service-stats">
${serviceStatRow('HTTP', httpStatus ? String(httpStatus) : '-', httpClass)}
${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')}
${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')}
</div>
</div>
`;
}
function renderDBCard(svc) {
const healthClass = svc.health_state === 'healthy' ? 'ok' : svc.health_state === 'unhealthy' ? 'bad' : '';
return `
<div class="service-card">
${serviceCardHeader(svc)}
<div class="service-stats">
${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')}
</div>
</div>
`;
}
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 `
<div class="service-card">
${serviceCardHeader(svc)}
<div class="service-stats">
${serviceStatRow('HTTP', httpStatus ? String(httpStatus) : '-', httpClass)}
${ms !== undefined ? serviceStatRow('Response', ms + 'ms', ms < 500 ? 'ok' : 'warn') : ''}
${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')}
</div>
</div>
`;
}
function renderMCPCard(svc) {
const extra = svc.extra || {};
const reachable = extra.port_reachable;
return `
<div class="service-card">
${serviceCardHeader(svc)}
<div class="service-stats">
${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), '')}
</div>
</div>
`;
}
function renderVoiceCard(svc) {
const healthClass = svc.health_state === 'healthy' ? 'ok' : svc.health_state === 'unhealthy' ? 'bad' : '';
return `
<div class="service-card">
${serviceCardHeader(svc)}
<div class="service-stats">
${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), '')}
</div>
</div>
`;
}
function renderAutomationCard(svc) {
const healthClass = svc.health_state === 'healthy' ? 'ok' : svc.health_state === 'unhealthy' ? 'bad' : '';
return `
<div class="service-card">
${serviceCardHeader(svc)}
<div class="service-stats">
${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), '')}
</div>
</div>
`;
}
function renderGenericServiceCard(svc) {
return `
<div class="service-card">
${serviceCardHeader(svc)}
<div class="service-stats">
${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')}
${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')}
</div>
</div> </div>
`; `;
} }
+3 -1
View File
@@ -9,13 +9,15 @@
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=Outfit:wght@300;400;500;600&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=Outfit:wght@300;400;500;600&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.min.css">
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<script>(function(){var t=localStorage.getItem('theme');if(t)document.documentElement.setAttribute('data-theme',t);})();</script>
</head> </head>
<body> <body>
<header> <header>
<div class="header-logo"> <div class="header-logo">
<h1><a href="/">agentmon<span class="logo-dot"></span></a></h1> <h1><a href="/">agentmon<span class="logo-dot"></span></a></h1>
</div> </div>
<nav><a href="/">Dashboard</a><a href="/sessions">Sessions</a><a href="/agents">Agents</a><a href="/openclaw">OpenClaw</a></nav> <nav><a href="/">Dashboard</a><a href="/sessions">Sessions</a><a href="/agents">Agents</a><a href="/infrastructure">Infra</a></nav>
<button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme"></button>
</header> </header>
<main id="app"> <main id="app">
<p>Loading...</p> <p>Loading...</p>