feat: rename OpenClaw to Infrastructure page, add service cards
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+260
-81
@@ -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;
|
||||||
|
|
||||||
|
const eventType = getEnvelopeType(msg.data);
|
||||||
|
|
||||||
|
if (eventType === 'openclaw.snapshot') {
|
||||||
|
mergeOpenClawEvents([msg.data]);
|
||||||
|
if (isCurrentPath('/infrastructure')) renderInfraGrid();
|
||||||
|
if (isCurrentPath('/agents')) renderAgentVMStrip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getEnvelopeType(msg.data) !== 'openclaw.snapshot') {
|
if (eventType === 'swarm.snapshot') {
|
||||||
|
mergeSwarmSnapshot(msg.data);
|
||||||
|
if (isCurrentPath('/infrastructure')) renderInfraGrid();
|
||||||
|
renderSwarmStrip_dash();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeOpenClawEvents([msg.data]);
|
if (eventType === 'swarm.service.snapshot') {
|
||||||
|
mergeSwarmServiceSnapshot(msg.data);
|
||||||
if (isCurrentPath('/openclaw')) {
|
if (isCurrentPath('/infrastructure')) renderInfraGrid();
|
||||||
renderOpenClawGrid();
|
renderSwarmStrip_dash();
|
||||||
}
|
return;
|
||||||
if (isCurrentPath('/agents')) {
|
|
||||||
renderAgentVMStrip();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,71 +749,231 @@
|
|||||||
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 => {
|
|
||||||
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 `
|
<div class="infra-section">
|
||||||
<div class="vm-card">
|
<p class="infra-section-title">VMs</p>
|
||||||
<div class="vm-card-header">
|
${vmNames.length === 0
|
||||||
<h3>${escapeHTML(inst.name || name)}</h3>
|
? '<p class="empty-state">No VM data</p>'
|
||||||
<div class="vm-status ${host.state === 'running' ? 'running' : 'stopped'}">
|
: `<div class="vm-grid">${vmNames.map(name => renderVMCard(name)).join('')}</div>`
|
||||||
${host.state === 'running' ? 'Running' : 'Stopped'}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="vm-updated">Updated ${escapeHTML(relativeTime(getEnvelopeTS(evt)))}</div>
|
<div class="infra-section">
|
||||||
<table class="vm-stats">
|
<p class="infra-section-title">Services</p>
|
||||||
<tr><td>Host</td><td>${escapeHTML(inst.host || '-')}</td></tr>
|
${services.length === 0
|
||||||
<tr><td>Domain</td><td>${escapeHTML(inst.domain || '-')}</td></tr>
|
? '<p class="empty-state">No swarm service data</p>'
|
||||||
<tr><td>vCPUs</td><td>${host.vcpus || '-'}</td></tr>
|
: `<div class="service-grid">${services.map(svc => renderServiceCard(svc)).join('')}</div>`
|
||||||
<tr><td>Memory</td><td>${escapeHTML(formatBytes(host.memory_kib ? host.memory_kib * 1024 : 0) || '-')}</td></tr>
|
}
|
||||||
<tr><td>Disk</td><td>${escapeHTML(formatBytes(host.disk_actual_bytes) || '-')}</td></tr>
|
</div>
|
||||||
<tr><td>Autostart</td><td>${host.autostart ? 'Yes' : 'No'}</td></tr>
|
`;
|
||||||
</table>
|
}
|
||||||
${guest ? `
|
|
||||||
<div class="vm-card-divider"></div>
|
function renderVMCard(name) {
|
||||||
<table class="vm-stats">
|
const evt = openclawState.instances[name];
|
||||||
<tr><td>Gateway</td><td style="${guest.service_active ? 'color:var(--success)' : 'color:var(--error)'}">${guest.service_active ? 'Active' : 'Inactive'}</td></tr>
|
const payload = getEnvelopePayload(evt);
|
||||||
<tr><td>HTTP</td><td style="${guest.http_status === 200 ? 'color:var(--success)' : 'color:var(--error)'}">${guest.http_status || 'N/A'}</td></tr>
|
const inst = payload.instance || {};
|
||||||
<tr><td>Version</td><td>${escapeHTML(guest.version || '-')}</td></tr>
|
const host = payload.host || {};
|
||||||
<tr><td>Guest Mem</td><td>${guest.memory_percent !== undefined ? guest.memory_percent.toFixed(1) : '-'}%</td></tr>
|
const guest = payload.guest;
|
||||||
<tr><td>Guest Disk</td><td>${guest.disk_percent !== undefined ? guest.disk_percent.toFixed(1) : '-'}%</td></tr>
|
const issues = payload.issues;
|
||||||
<tr><td>Load</td><td>${guest.load_average !== undefined ? guest.load_average.toFixed(2) : '-'}</td></tr>
|
|
||||||
<tr><td>Uptime</td><td>${escapeHTML(guest.service_uptime || '-')}</td></tr>
|
return `
|
||||||
</table>
|
<div class="vm-card">
|
||||||
` : ''}
|
<div class="vm-card-header">
|
||||||
${issues && Object.values(issues).some(Boolean) ? `
|
<h3>${escapeHTML(inst.name || name)}</h3>
|
||||||
<div class="vm-card-divider"></div>
|
<div class="vm-status ${host.state === 'running' ? 'running' : 'stopped'}">
|
||||||
<div class="vm-issues-label">Issues</div>
|
${host.state === 'running' ? 'Running' : 'Stopped'}
|
||||||
<div class="vm-issues">
|
</div>
|
||||||
${Object.entries(issues).filter(([, value]) => value).map(([key]) => `
|
</div>
|
||||||
<span class="issue ${escapeHTML(key)}">${escapeHTML(key.replace(/_/g, ' '))}</span>
|
<div class="vm-updated">Updated ${escapeHTML(relativeTime(getEnvelopeTS(evt)))}</div>
|
||||||
`).join('')}
|
<table class="vm-stats">
|
||||||
</div>
|
<tr><td>Host</td><td>${escapeHTML(inst.host || '-')}</td></tr>
|
||||||
` : ''}
|
<tr><td>Domain</td><td>${escapeHTML(inst.domain || '-')}</td></tr>
|
||||||
</div>
|
<tr><td>vCPUs</td><td>${host.vcpus || '-'}</td></tr>
|
||||||
`;
|
<tr><td>Memory</td><td>${escapeHTML(formatBytes(host.memory_kib ? host.memory_kib * 1024 : 0) || '-')}</td></tr>
|
||||||
}).join('')}
|
<tr><td>Disk</td><td>${escapeHTML(formatBytes(host.disk_actual_bytes) || '-')}</td></tr>
|
||||||
|
<tr><td>Autostart</td><td>${host.autostart ? 'Yes' : 'No'}</td></tr>
|
||||||
|
</table>
|
||||||
|
${guest ? `
|
||||||
|
<div class="vm-card-divider"></div>
|
||||||
|
<table class="vm-stats">
|
||||||
|
<tr><td>Gateway</td><td style="${guest.service_active ? 'color:var(--success)' : 'color:var(--error)'}">${guest.service_active ? 'Active' : 'Inactive'}</td></tr>
|
||||||
|
<tr><td>HTTP</td><td style="${guest.http_status === 200 ? 'color:var(--success)' : 'color:var(--error)'}">${guest.http_status || 'N/A'}</td></tr>
|
||||||
|
<tr><td>Version</td><td>${escapeHTML(guest.version || '-')}</td></tr>
|
||||||
|
<tr><td>Guest Mem</td><td>${guest.memory_percent !== undefined ? guest.memory_percent.toFixed(1) : '-'}%</td></tr>
|
||||||
|
<tr><td>Guest Disk</td><td>${guest.disk_percent !== undefined ? guest.disk_percent.toFixed(1) : '-'}%</td></tr>
|
||||||
|
<tr><td>Load</td><td>${guest.load_average !== undefined ? guest.load_average.toFixed(2) : '-'}</td></tr>
|
||||||
|
<tr><td>Uptime</td><td>${escapeHTML(guest.service_uptime || '-')}</td></tr>
|
||||||
|
</table>
|
||||||
|
` : ''}
|
||||||
|
${issues && Object.values(issues).some(Boolean) ? `
|
||||||
|
<div class="vm-card-divider"></div>
|
||||||
|
<div class="vm-issues-label">Issues</div>
|
||||||
|
<div class="vm-issues">
|
||||||
|
${Object.entries(issues).filter(([, value]) => value).map(([key]) => `
|
||||||
|
<span class="issue ${escapeHTML(key)}">${escapeHTML(key.replace(/_/g, ' '))}</span>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user