// ── state.js — shared live state for openclawState, swarmState, agentsState import { getEnvelopePayload, getEnvelopeTS, normalizeAgentKey, } from './utils.js'; // ── OpenClaw & Swarm ───────────────────────────────────── export let openclawState = { instances: {} }; export let swarmState = { services: {} }; export function mergeOpenClawEvents(events) { for (const evt of events) { const payload = getEnvelopePayload(evt); const instance = payload.instance || {}; if (!instance.name) continue; const existing = openclawState.instances[instance.name]; const nextTS = new Date(getEnvelopeTS(evt) || 0).getTime(); const currentTS = existing ? new Date(getEnvelopeTS(existing) || 0).getTime() : 0; if (!existing || nextTS >= currentTS) { openclawState.instances[instance.name] = evt; } } } export function mergeSwarmSnapshot(evt) { const payload = getEnvelopePayload(evt); const services = payload.services || []; for (const svc of services) { if (svc.name) swarmState.services[svc.name] = svc; } } export function mergeSwarmServiceSnapshot(evt) { const payload = getEnvelopePayload(evt); const svc = payload.service; if (svc && svc.name) swarmState.services[svc.name] = svc; } export function getK8sHomelabServices() { const services = []; for (const [name, evt] of Object.entries(openclawState.instances)) { const payload = getEnvelopePayload(evt); if (!payload.minio) continue; const minio = payload.minio; services.push({ name: 'minio-storage', role: 'storage', category: 'k8s homelab', sourceInstance: name, status: minio.reachable ? 'healthy' : 'down', endpoint: minio.endpoint || '', bucket: minio.bucket || '', prefix: minio.prefix || '', objectCount: minio.object_count, totalBytes: minio.total_bytes, latestBackup: minio.latest_backup || '', httpStatus: minio.http_status, error: minio.error || '', }); } return services; } export function getVMStatus() { const names = Object.keys(openclawState.instances).sort(); return names.map(name => { const snapshot = openclawState.instances[name]; const payload = snapshot ? getEnvelopePayload(snapshot) : {}; const host = payload.host || {}; return { name, active: host.state === 'running' }; }); } export function getDashboardInfraPill() { const services = Object.values(swarmState.services); if (services.length === 0) { return { className: 'inactive', name: 'infra', label: 'unknown' }; } const unhealthy = services.filter(svc => svc.status !== 'healthy'); if (unhealthy.length === 0) { return { className: 'active', name: 'infra', label: 'all running' }; } const degradedOnly = unhealthy.every(svc => svc.status === 'degraded'); return { className: degradedOnly ? 'degraded' : 'inactive', name: 'infra', label: degradedOnly ? 'degraded' : `${unhealthy.length} issue${unhealthy.length === 1 ? '' : 's'}`, }; } export function isOpenClawVM(agent) { const key = normalizeAgentKey(agent && agent.name); return !!openclawState.instances[key]; } export function isAgentOnline(agent) { if (!agent) return false; if (isOpenClawVM(agent)) { const vmStatus = getVMStatus().find(v => v.name === normalizeAgentKey(agent.name)); if (vmStatus) return vmStatus.active; } const hasSessions = Object.keys(agent.sessions).length > 0; const hasOps = Object.keys(agent.operations).length > 0; const seenRecently = agent.lastSeenAt > 0 && (Date.now() - agent.lastSeenAt) < 300000; return hasSessions || hasOps || seenRecently; } // ── Agents state ───────────────────────────────────────── function createAgentsState() { return { agents: {}, stats: { messages: 0, tools: 0, errors: 0, toolCounts: {} }, dbStats: { messages: 0, tools: 0, errors: 0 }, viewMode: 'overview', selectedAgentKey: '', timerInterval: null, }; } export let agentsState = createAgentsState(); export function resetAgentsState() { agentsState = createAgentsState(); }