feat: add agentmon services section to infrastructure page

Label all agentmon docker-compose services with agentmon.monitor=true
and agentmon.group=agentmon so the swarm-monitor picks them up.
Adds Group field to ServiceSnapshot, probes /healthz for api/web roles,
and renders a separate "Agentmon" section below Swarm Services on the
Infrastructure page with new api and worker card renderers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-03-18 13:41:26 -07:00
parent d2d044a3d8
commit f8ddea3698
7 changed files with 147 additions and 6 deletions
+13
View File
@@ -218,6 +218,19 @@ func main() {
httpx.WriteJSON(w, http.StatusOK, summary)
})
r.Get("/v1/stats/top-tools", func(w http.ResponseWriter, r *http.Request) {
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
tools, err := db.GetTopTools(r.Context(), limit)
if err != nil {
httpx.WriteJSON(w, http.StatusInternalServerError, map[string]any{"error": "db_error"})
return
}
if tools == nil {
tools = []postgres.TopTool{}
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"tools": tools})
})
r.Get("/v1/stats/timeseries", func(w http.ResponseWriter, r *http.Request) {
window := r.URL.Query().Get("window")
switch window {
+45 -4
View File
@@ -751,7 +751,9 @@
function renderInfraGrid() {
const vmNames = Object.keys(openclawState.instances).sort();
const services = Object.values(swarmState.services);
const allServices = Object.values(swarmState.services);
const agentmonServices = allServices.filter(s => s.group === 'agentmon');
const swarmServices = allServices.filter(s => s.group !== 'agentmon');
app.innerHTML = `
<div class="page-header">
@@ -767,10 +769,18 @@
</div>
<div class="infra-section">
<p class="infra-section-title">Services</p>
${services.length === 0
<p class="infra-section-title">Swarm Services</p>
${swarmServices.length === 0
? '<p class="empty-state">No swarm service data</p>'
: `<div class="service-grid">${services.map(svc => renderServiceCard(svc)).join('')}</div>`
: `<div class="service-grid">${swarmServices.map(svc => renderServiceCard(svc)).join('')}</div>`
}
</div>
<div class="infra-section">
<p class="infra-section-title">Agentmon</p>
${agentmonServices.length === 0
? '<p class="empty-state">No agentmon service data</p>'
: `<div class="service-grid">${agentmonServices.map(svc => renderServiceCard(svc)).join('')}</div>`
}
</div>
`;
@@ -835,6 +845,10 @@
case 'mcp': return renderMCPCard(svc);
case 'voice': return renderVoiceCard(svc);
case 'automation':return renderAutomationCard(svc);
case 'api':
case 'web': return renderAPICard(svc);
case 'worker':
case 'queue': return renderWorkerCard(svc);
default: return renderGenericServiceCard(svc);
}
}
@@ -966,6 +980,33 @@
`;
}
function renderAPICard(svc) {
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)}
${serviceStatRow('Uptime', formatUptime(svc.uptime_sec), '')}
${serviceStatRow('Container', escapeHTML(svc.container_state || '-'), svc.container_state === 'running' ? 'ok' : 'bad')}
</div>
</div>
`;
}
function renderWorkerCard(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>
`;
}
function renderGenericServiceCard(svc) {
return `
<div class="service-card">