(function() { // ── Theme toggle ───────────────────────────────────────── const THEME_CYCLE = ['system', 'light', 'dark']; const THEME_ICONS = { system: '', light: '', dark: '', }; const THEME_LABELS = { system: 'System theme', light: 'Light theme', dark: 'Dark theme' }; function getTheme() { return localStorage.getItem('theme') || 'system'; } function applyTheme(theme) { if (theme === 'system') document.documentElement.removeAttribute('data-theme'); else document.documentElement.setAttribute('data-theme', theme); } function updateToggleBtn(theme) { const btn = document.getElementById('theme-toggle'); if (!btn) return; btn.innerHTML = THEME_ICONS[theme]; btn.title = THEME_LABELS[theme]; } function cycleTheme() { const next = THEME_CYCLE[(THEME_CYCLE.indexOf(getTheme()) + 1) % THEME_CYCLE.length]; if (next === 'system') localStorage.removeItem('theme'); else localStorage.setItem('theme', next); applyTheme(next); updateToggleBtn(next); } document.addEventListener('DOMContentLoaded', function() { updateToggleBtn(getTheme()); const btn = document.getElementById('theme-toggle'); if (btn) btn.addEventListener('click', cycleTheme); }); // ───────────────────────────────────────────────────────── const app = document.getElementById('app'); let ws = null; let wsStatus = 'disconnected'; let wsReconnectTimeout = null; let wsReconnectDelay = 1000; const wsCallbacks = new Set(); let sessionsState = { sessions: [], cursor: null, activeSessionByBackend: {} }; 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; let dashboardState = null; let dashboardUnsubscribe = null; let dashboardChart = null; let dashboardResizeObserver = null; const DASH_RECENT_EVENTS_LIMIT = 10; const DASH_RECENT_EVENTS_STORAGE_KEY = 'agentmon:dash:recent-events'; function getDashboardChartMode() { const mode = localStorage.getItem('agentmon:dash:chart-mode'); return mode === 'lines' ? 'lines' : 'stacked'; } function getWsURL() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return protocol + '//' + window.location.host + '/api/v1/ws'; } function connectWS() { if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { return; } try { ws = new WebSocket(getWsURL()); ws.onopen = () => { console.log('WebSocket connected'); wsStatus = 'connected'; wsReconnectDelay = 1000; updateWSIndicator(); wsCallbacks.forEach(cb => cb({ type: 'connected' })); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); wsCallbacks.forEach(cb => cb({ type: 'message', data })); } catch (e) { console.error('Failed to parse WS message:', e); } }; ws.onclose = () => { console.log('WebSocket disconnected'); wsStatus = 'reconnecting'; updateWSIndicator(); wsCallbacks.forEach(cb => cb({ type: 'disconnected' })); wsReconnectTimeout = setTimeout(connectWS, wsReconnectDelay); wsReconnectDelay = Math.min(wsReconnectDelay * 1.5, 30000); }; ws.onerror = (err) => { console.error('WebSocket error:', err); }; } catch (e) { console.error('Failed to connect WebSocket:', e); wsReconnectTimeout = setTimeout(connectWS, wsReconnectDelay); wsReconnectDelay = Math.min(wsReconnectDelay * 1.5, 30000); } } function subscribeWS(callback) { wsCallbacks.add(callback); if (!ws || ws.readyState !== WebSocket.OPEN) { connectWS(); } return () => wsCallbacks.delete(callback); } function updateWSIndicator() { const dot = document.getElementById('ws-dot'); if (!dot) return; dot.className = 'ws-dot ' + wsStatus; const labels = { connected: 'Live — WebSocket connected', reconnecting: 'Reconnecting…', disconnected: 'Disconnected', }; dot.title = labels[wsStatus] || 'Unknown'; } function cleanupLiveViews() { if (openclawUnsubscribe) { openclawUnsubscribe(); openclawUnsubscribe = null; } if (infraUnsubscribe) { infraUnsubscribe(); infraUnsubscribe = null; } if (agentsUnsubscribe) { agentsUnsubscribe(); agentsUnsubscribe = null; } if (sessionsState && sessionsState.timerInterval) { clearInterval(sessionsState.timerInterval); sessionsState.timerInterval = null; } if (sessionsUnsubscribe) { sessionsUnsubscribe(); sessionsUnsubscribe = null; } if (dashboardUnsubscribe) { dashboardUnsubscribe(); dashboardUnsubscribe = null; } if (dashboardChart) { dashboardChart.destroy(); dashboardChart = null; } if (dashboardResizeObserver) { dashboardResizeObserver.disconnect(); dashboardResizeObserver = null; } if (agentsState && agentsState.timerInterval) { clearInterval(agentsState.timerInterval); agentsState.timerInterval = null; } if (_agentsRenderTimer) { cancelAnimationFrame(_agentsRenderTimer); _agentsRenderTimer = null; } if (_dashFeedRenderTimer) { cancelAnimationFrame(_dashFeedRenderTimer); _dashFeedRenderTimer = null; } } function route() { cleanupLiveViews(); const path = window.location.pathname; if (path === '/') { renderDashboard(); } else if (path === '/sessions') { renderSessions(); } else if (path.startsWith('/agents')) { renderAgents(); } else if (path.startsWith('/infrastructure')) { renderInfrastructure(); } else if (path.startsWith('/sessions/')) { renderSession(path.split('/sessions/')[1]); } else if (path.startsWith('/runs/')) { renderRun(path.split('/runs/')[1]); } else { app.innerHTML = '

Page not found

The page you\'re looking for doesn\'t exist.

Go to Dashboard
'; } updateActiveNav(); } function navigate(path) { history.pushState(null, '', path); route(); } function updateActiveNav() { const path = window.location.pathname; document.querySelectorAll('header nav a').forEach(a => { const href = a.getAttribute('href'); const isActive = href === '/' ? path === '/' : path.startsWith(href); a.classList.toggle('active', isActive); }); } window.addEventListener('popstate', route); async function api(path) { const resp = await fetch('/api' + path); if (!resp.ok) { const body = await resp.json().catch(() => ({})); const msg = body.error || 'Request failed (' + resp.status + ')'; showToast(msg, 'error'); throw new Error(msg); } return resp.json(); } function showToast(message, type) { const existing = document.querySelector('.toast'); if (existing) existing.remove(); const toast = document.createElement('div'); toast.className = 'toast toast-' + (type || 'info'); toast.textContent = message; document.body.appendChild(toast); requestAnimationFrame(() => toast.classList.add('visible')); setTimeout(() => { toast.classList.remove('visible'); setTimeout(() => toast.remove(), 300); }, 4000); } function tryParseJSON(s) { try { return s ? JSON.parse(s) : null; } catch { return null; } } function escapeHTML(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function relativeTime(ts) { if (!ts) { return '-'; } const now = Date.now(); const then = new Date(ts).getTime(); const diff = now - then; if (diff < 60000) return 'just now'; if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago'; if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago'; return Math.floor(diff / 86400000) + 'd ago'; } function formatDuration(ms) { if (ms === undefined || ms === null || ms === '') return '-'; if (ms < 1000) return ms + 'ms'; if (ms < 60000) return (ms / 1000).toFixed(1) + 's'; return (ms / 60000).toFixed(1) + 'm'; } function formatBytes(bytes) { if (!bytes) return null; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let unitIndex = 0; let value = bytes; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex++; } return value.toFixed(1) + ' ' + units[unitIndex]; } function statusIcon(status) { if (status === 'success') return 'success'; if (status === 'error') return 'error'; return 'unknown'; } function skeletonRows(rows, cols) { return Array(rows).fill(0).map(() => '' + Array(cols).fill('
').join('') + '' ).join(''); } function extractEnvelope(record) { if (record && typeof record === 'object' && record.payload && record.payload.event && record.payload.schema) { return record.payload; } return record || {}; } function getEnvelopeEvent(record) { const envelope = extractEnvelope(record); return envelope.event || envelope.Event || {}; } function getEnvelopeType(record) { return record?.type || getEnvelopeEvent(record).type || ''; } function getEnvelopeTS(record) { return record?.ts || getEnvelopeEvent(record).ts || ''; } function getEnvelopeSource(record) { return getEnvelopeEvent(record).source || {}; } function getEnvelopePayload(record) { const envelope = extractEnvelope(record); return envelope.payload || envelope.Payload || {}; } function getEnvelopeAttributes(record) { const envelope = extractEnvelope(record); return envelope.attributes || envelope.Attributes || {}; } function getEnvelopeCorrelation(record) { const envelope = extractEnvelope(record); return envelope.correlation || envelope.Correlation || {}; } function getRecordID(record) { return record?.event_id || getEnvelopeEvent(record).id || ''; } function isCurrentPath(prefix) { return window.location.pathname.startsWith(prefix); } function groupSessionsByDate(sessions) { const now = new Date(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const yesterdayStart = new Date(todayStart); yesterdayStart.setDate(yesterdayStart.getDate() - 1); const weekStart = new Date(todayStart); weekStart.setDate(weekStart.getDate() - 6); const groups = [ { label: 'Today', items: [] }, { label: 'Yesterday', items: [] }, { label: 'This Week', items: [] }, { label: 'Older', items: [] }, ]; for (const s of sessions) { const d = new Date(s.started_at); if (d >= todayStart) groups[0].items.push(s); else if (d >= yesterdayStart) groups[1].items.push(s); else if (d >= weekStart) groups[2].items.push(s); else groups[3].items.push(s); } return groups.filter(g => g.items.length > 0); } function getSessionBackendKey(s) { const framework = s.framework || 'unknown'; const backendID = s.client_id || s.host || 'unknown'; return `${framework}|${backendID}`; } function sessionActivityTS(s) { const raw = s._lastActivityTS || Date.parse(s.started_at); return Number.isFinite(raw) ? raw : 0; } function recomputeActiveSessionByBackend() { const next = {}; const bestTS = {}; sessionsState.sessions.forEach(s => { if (!isSessionActive(s)) return; const key = getSessionBackendKey(s); const ts = sessionActivityTS(s); if (!next[key] || ts > bestTS[key]) { next[key] = s.session_id; bestTS[key] = ts; } }); sessionsState.activeSessionByBackend = next; } function sessionDotState(s) { if (!isSessionActive(s)) return 'ended'; const key = getSessionBackendKey(s); const activeSessionID = sessionsState.activeSessionByBackend[key]; return activeSessionID === s.session_id ? 'active' : 'idle'; } function touchSessionActivity(sessionID, ts, source) { const session = sessionsState.sessions.find(s => s.session_id === sessionID); if (!session) return null; const parsedTS = Date.parse(ts || ''); const activityTS = Number.isFinite(parsedTS) ? parsedTS : Date.now(); session._lastActivityTS = Math.max(session._lastActivityTS || 0, activityTS); if (source && typeof source === 'object') { if (source.framework) session.framework = source.framework; if (source.host) session.host = source.host; if (source.client_id) session.client_id = source.client_id; } const key = getSessionBackendKey(session); sessionsState.activeSessionByBackend[key] = session.session_id; return session; } function refreshSessionsTable() { const tbody = document.getElementById('sessions-body'); if (!tbody) return; const groups = groupSessionsByDate(sessionsState.sessions); if (groups.length === 0) { tbody.innerHTML = 'No sessions found'; return; } tbody.innerHTML = groups.map(group => { const rows = group.items.map(s => { const fw = s.framework || 'unknown'; const fwClass = fw.replace(/[^a-z0-9-]/g, '-'); const active = isSessionActive(s); const dotState = sessionDotState(s); const dotTitle = dotState === 'active' ? 'Currently active session' : (active ? 'Open session' : 'Session ended'); return ` ${escapeHTML(s.session_id.substring(0, 12))}… ${escapeHTML(fw)} ${escapeHTML(s.host || '-')} ${s.run_count} ${escapeHTML(relativeTime(s.started_at))} `; }).join(''); return `${escapeHTML(group.label)}${rows}`; }).join(''); tbody.querySelectorAll('tr.clickable').forEach(row => { row.addEventListener('click', () => navigate('/sessions/' + row.dataset.session)); }); } async function renderSessions() { app.innerHTML = `
${skeletonRows(8, 5)}
Session Framework Host Runs Time
`; api('/v1/stats/summary').then(data => { const sel = document.getElementById('filter-framework'); if (!sel || !data.by_framework) return; for (const fw of Object.keys(data.by_framework).sort()) { const opt = document.createElement('option'); opt.value = fw; opt.textContent = fw; sel.appendChild(opt); } }).catch(() => {}); // Restore filters from URL const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('from')) document.getElementById('filter-from').value = urlParams.get('from'); if (urlParams.get('to')) document.getElementById('filter-to').value = urlParams.get('to'); if (urlParams.get('framework')) document.getElementById('filter-framework').value = urlParams.get('framework'); if (urlParams.get('host')) document.getElementById('filter-host').value = urlParams.get('host'); ['from', 'to', 'framework'].forEach(f => { document.getElementById('filter-' + f).addEventListener('change', () => { sessionsState.sessions = []; sessionsState.cursor = null; loadSessions(); }); }); let _hostDebounce = null; document.getElementById('filter-host').addEventListener('input', () => { clearTimeout(_hostDebounce); _hostDebounce = setTimeout(() => { sessionsState.sessions = []; sessionsState.cursor = null; loadSessions(); }, 400); }); document.getElementById('load-more').addEventListener('click', loadSessions); sessionsState = { sessions: [], cursor: null, timerInterval: null, activeSessionByBackend: {} }; await loadSessions(); sessionsState.timerInterval = setInterval(updateSessionTimers, 30000); sessionsUnsubscribe = subscribeWS(handleSessionsWS); } function isSessionActive(s) { return !s.ended_at; } function renderSessionRow(s) { const fw = s.framework || 'unknown'; const fwClass = fw.replace(/[^a-z0-9-]/g, '-'); const active = isSessionActive(s); const dotState = sessionDotState(s); const dotTitle = dotState === 'active' ? 'Currently active session' : (active ? 'Open session' : 'Session ended'); return ` ${escapeHTML(s.session_id.substring(0, 12))}… ${escapeHTML(fw)} ${escapeHTML(s.host || '-')} ${s.run_count} ${escapeHTML(relativeTime(s.started_at))} `; } function updateSessionTimers() { const tbody = document.getElementById('sessions-body'); if (!tbody) return; sessionsState.sessions.forEach(s => { const row = tbody.querySelector(`[data-session="${s.session_id}"]`); if (row) { const td = row.cells[4]; if (td) td.textContent = relativeTime(s.started_at); } }); } function handleSessionsWS(msg) { if (msg.type !== 'message') return; const eventType = getEnvelopeType(msg.data); const correlation = getEnvelopeCorrelation(msg.data); const source = getEnvelopeSource(msg.data); const ts = getEnvelopeTS(msg.data); const sessionId = correlation?.session_id || msg.data.event?.id; if (eventType === 'session.start') { const newSession = { session_id: sessionId, started_at: ts || new Date().toISOString(), framework: source.framework || 'unknown', client_id: source.client_id || '', host: source.host || '-', run_count: 1, _lastActivityTS: Date.parse(ts || '') || Date.now(), }; sessionsState.sessions.unshift(newSession); const backendKey = getSessionBackendKey(newSession); sessionsState.activeSessionByBackend[backendKey] = newSession.session_id; refreshSessionsTable(); return; } const tbody = document.getElementById('sessions-body'); if (!tbody) return; if (sessionId) { touchSessionActivity(sessionId, ts, source); } if (eventType === 'run.start' && sessionId) { const session = sessionsState.sessions.find(s => s.session_id === sessionId); if (session) { session.run_count = (session.run_count || 0) + 1; const row = tbody.querySelector(`[data-session="${sessionId}"]`); if (row && row.cells[3]) row.cells[3].textContent = session.run_count; } } if (eventType === 'session.end' && sessionId) { const session = sessionsState.sessions.find(s => s.session_id === sessionId); if (session) { session.ended_at = new Date().toISOString(); recomputeActiveSessionByBackend(); } } refreshSessionsTable(); } async function loadSessions() { const params = new URLSearchParams(); const from = document.getElementById('filter-from').value; const to = document.getElementById('filter-to').value; const framework = document.getElementById('filter-framework').value; const host = document.getElementById('filter-host').value; // Sync filters to URL const filterParams = new URLSearchParams(); if (from) filterParams.set('from', from); if (to) filterParams.set('to', to); if (framework) filterParams.set('framework', framework); if (host) filterParams.set('host', host); const filterQS = filterParams.toString(); const newURL = '/sessions' + (filterQS ? '?' + filterQS : ''); if (window.location.pathname + window.location.search !== newURL) { history.replaceState(null, '', newURL); } if (from) params.set('from', from); if (to) params.set('to', to); if (framework) params.set('framework', framework); if (host) params.set('host', host); if (sessionsState.cursor) params.set('cursor', sessionsState.cursor); const data = await api('/v1/sessions?' + params.toString()); const incoming = (data.sessions || []).map(s => ({ ...s, _lastActivityTS: Date.parse(s.started_at || '') || Date.now(), })); sessionsState.sessions = sessionsState.sessions.concat(incoming); sessionsState.cursor = data.next_cursor; recomputeActiveSessionByBackend(); refreshSessionsTable(); document.getElementById('load-more').style.display = sessionsState.cursor ? 'block' : 'none'; } async function renderSession(sessionID) { const data = await api('/v1/sessions/' + sessionID); const s = data.session; const runs = data.runs || []; const active = !s.ended_at; const duration = s.ended_at ? formatDuration(new Date(s.ended_at) - new Date(s.started_at)) : 'ongoing'; app.innerHTML = ` ← Back to Sessions
Runs ${runs.length}
${renderSessionRunsRows(runs)}
Run ID Status Model Tools Spans Duration Started
`; bindSessionRunRows(); document.querySelector('.back-link').addEventListener('click', e => { e.preventDefault(); navigate('/sessions'); }); sessionsUnsubscribe = subscribeWS((msg) => handleSessionWS(sessionID, msg)); } let _sessionReloadTimer = null; function handleSessionWS(sessionID, msg) { if (msg.type !== 'message') return; const correlation = getEnvelopeCorrelation(msg.data); if (correlation?.session_id !== sessionID) return; const eventType = getEnvelopeType(msg.data); if (!['run.start', 'run.end', 'session.end', 'error'].includes(eventType)) return; clearTimeout(_sessionReloadTimer); _sessionReloadTimer = setTimeout(() => loadSessionData(sessionID), 300); } async function loadSessionData(sessionID) { if (!isCurrentPath('/sessions/' + sessionID)) return; const data = await api('/v1/sessions/' + sessionID); const runs = data.runs || []; const tbody = document.getElementById('session-runs-body'); if (!tbody) return; tbody.innerHTML = renderSessionRunsRows(runs); bindSessionRunRows(); const countSpan = document.querySelector('.section-title .count'); if (countSpan) countSpan.textContent = runs.length; } function renderSpanPayload(sp) { const outer = sp.payload || {}; const inner = outer.payload || {}; const parts = []; if (sp.kind === 'tool') { if (inner.input !== undefined) { const inputStr = typeof inner.input === 'object' ? JSON.stringify(inner.input, null, 2) : String(inner.input); parts.push(`
Input
${escapeHTML(inputStr)}
`); } if (inner.result_preview !== undefined) { parts.push(`
Result
${escapeHTML(String(inner.result_preview))}
`); } } else if (sp.kind === 'agent') { if (inner.prompt_preview) { parts.push(`
Prompt
${escapeHTML(String(inner.prompt_preview))}
`); } if (inner.usage) { const u = inner.usage; const tokens = [ u.total_tokens != null ? `${u.total_tokens} total` : null, u.input_tokens != null ? `${u.input_tokens} in` : null, u.output_tokens != null ? `${u.output_tokens} out` : null, ].filter(Boolean).join(' · '); if (tokens) parts.push(`
Tokens${escapeHTML(tokens)}
`); if (u.total_cost != null) { parts.push(`
Cost${escapeHTML(formatCost(u.total_cost))}
`); } } if (inner.model) { parts.push(`
Model${escapeHTML(String(inner.model))}
`); } } else { const raw = Object.keys(inner).length > 0 ? inner : (Object.keys(outer).length > 0 ? outer : null); if (raw) { parts.push(`
${escapeHTML(JSON.stringify(raw, null, 2))}
`); } } if (sp.duration_ms != null) { parts.push(`
Duration${escapeHTML(formatDuration(sp.duration_ms))}
`); } return parts.length > 0 ? parts.join('') : 'No payload data'; } function renderRunSpansRows(spans) { if (!spans || spans.length === 0) { return 'No spans'; } return spans.map((sp, i) => { const kindClass = sp.kind || 'unknown'; return ` ${escapeHTML(sp.kind || '?')} ${escapeHTML(sp.name || '(unnamed)')} ${escapeHTML(sp.kind || '-')} ${statusIcon(sp.status)} ${escapeHTML(formatDuration(sp.duration_ms))}
${renderSpanPayload(sp)}
`; }).join(''); } function bindRunSpanRows() { document.querySelectorAll('tr.run-span-row').forEach(row => { row.addEventListener('click', () => { const idx = row.dataset.index; const detailRow = document.querySelector(`tr.span-detail-row[data-index="${idx}"]`); const icon = row.querySelector('.expand-icon'); if (!detailRow) return; const isOpen = detailRow.style.display !== 'none'; detailRow.style.display = isOpen ? 'none' : 'table-row'; if (icon) icon.style.transform = isOpen ? '' : 'rotate(45deg)'; }); row.setAttribute('tabindex', '0'); row.setAttribute('role', 'button'); row.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); row.click(); } }); }); } // Track active spans for the run detail live ops panel let runLiveOps = {}; // spanID → { name, kind, startedAt, promptPreview, inputPreview } let _runReloadTimer = null; let _dashFeedRenderTimer = null; async function renderRun(runID) { app.innerHTML = '
' + '
' + skeletonRows(5, 4) + '
NameKindStatusDuration
'; runLiveOps = {}; let data; try { data = await api('/v1/runs/' + runID); } catch (e) { app.innerHTML = `

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

`; return; } const r = data.run; const spans = data.spans || []; const duration = r.ended_at ? formatDuration(new Date(r.ended_at) - new Date(r.started_at)) : 'ongoing'; app.innerHTML = ` ← Back to Session ${!r.ended_at ? '
' : ''}
Spans ${spans.length} ${!r.ended_at ? 'Live' : ''}
${renderRunSpansRows(spans)}
Name Kind Status Duration
`; bindRunSpanRows(); document.querySelector('.back-link').addEventListener('click', e => { e.preventDefault(); navigate('/sessions/' + r.session_id); }); if (!r.ended_at) { sessionsUnsubscribe = subscribeWS((msg) => handleRunWS(runID, msg)); } } function renderRunLiveOps() { const el = document.getElementById('run-live-ops'); if (!el) return; const ops = Object.values(runLiveOps); if (ops.length === 0) { el.innerHTML = ''; return; } el.innerHTML = `
${ops.map(op => { const elapsed = Math.floor((Date.now() - op.startedAt) / 1000); const isSubagent = op.kind === 'agent' || op.subType === 'subagent'; const icon = isSubagent ? '◎' : op.kind === 'run' ? '◌' : '▸'; const label = isSubagent ? 'subagent' : op.kind === 'run' ? 'thinking' : 'tool'; const preview = op.promptPreview || op.inputPreview || ''; return `
${icon} ${escapeHTML(op.name)} ${preview ? `${escapeHTML(preview.length > 60 ? preview.slice(0, 60) + '…' : preview)}` : ''} ${formatElapsed(elapsed)}
`; }).join('')}
`; } function handleRunWS(runID, msg) { if (msg.type !== 'message') return; const correlation = getEnvelopeCorrelation(msg.data); if (correlation?.run_id !== runID) return; // Track live ops from WS without full reload const eventType = getEnvelopeType(msg.data); const attrs = getEnvelopeAttributes(msg.data); const payload = getEnvelopePayload(msg.data); const spanID = correlation.span_id; if (eventType === 'span.start' && spanID) { runLiveOps[spanID] = { name: attrs.name || attrs.span_kind || 'span', kind: attrs.span_kind || '', subType: attrs.type || '', startedAt: Date.now(), promptPreview: payload.prompt_preview || '', inputPreview: payload.input ? (typeof payload.input === 'string' ? payload.input.slice(0, 100) : '') : '', }; renderRunLiveOps(); } if (eventType === 'span.end' && spanID) { delete runLiveOps[spanID]; renderRunLiveOps(); } if (eventType === 'run.start') { runLiveOps['__run__'] = { name: 'Thinking…', kind: 'run', startedAt: Date.now(), promptPreview: payload.prompt_preview || payload.message_preview || payload.message || '', inputPreview: '', }; renderRunLiveOps(); } if (eventType === 'run.end') { delete runLiveOps['__run__']; runLiveOps = {}; renderRunLiveOps(); } clearTimeout(_runReloadTimer); _runReloadTimer = setTimeout(() => loadRunDetailData(runID), 500); } async function loadRunDetailData(runID) { if (!isCurrentPath('/runs/' + runID)) return; try { const data = await api('/v1/runs/' + runID); const spans = data.spans || []; const r = data.run; const tbody = document.getElementById('spans-body'); if (!tbody) return; // Preserve expanded rows const openIndices = new Set(); document.querySelectorAll('tr.span-detail-row').forEach(row => { if (row.style.display !== 'none') openIndices.add(row.dataset.index); }); tbody.innerHTML = renderRunSpansRows(spans); // Restore expanded rows document.querySelectorAll('tr.span-detail-row').forEach(row => { if (openIndices.has(row.dataset.index)) { row.style.display = 'table-row'; const hdr = document.querySelector(`tr.run-span-row[data-index="${row.dataset.index}"]`); if (hdr) { const icon = hdr.querySelector('.expand-icon'); if (icon) icon.style.transform = 'rotate(45deg)'; } } }); bindRunSpanRows(); const countEl = document.getElementById('run-detail-span-count'); if (countEl) countEl.textContent = spans.length; if (r.ended_at) { const durEl = document.getElementById('run-detail-duration'); if (durEl) durEl.textContent = formatDuration(new Date(r.ended_at) - new Date(r.started_at)); if (sessionsUnsubscribe) { sessionsUnsubscribe(); sessionsUnsubscribe = null; } const liveSpan = document.querySelector('.section-title .live-indicator'); if (liveSpan) liveSpan.remove(); } } catch (e) { console.error('Failed to reload run detail:', e); } } function renderSessionRunsRows(runs) { if (!runs || runs.length === 0) { return 'No runs'; } return runs.map((r, i) => { const runDuration = r.ended_at ? formatDuration(new Date(r.ended_at) - new Date(r.started_at)) : '-'; const modelLabel = r.model ? escapeHTML(r.model.replace(/^claude-/, '')) : '-'; const spans = r.spans || []; const spansHTML = spans.length > 0 ? `
${spans.map(sp => { const body = getSessionSpanSummary(sp); return `
${escapeHTML(sp.name || sp.kind || 'span')} ${escapeHTML(body)}
`; }).join('')}
` : '
No spans yet
'; return ` ${escapeHTML(r.run_id.substring(0, 12))}... ${statusIcon(r.status)} ${modelLabel} ${r.tool_count || 0} ${r.span_count} ${escapeHTML(runDuration)} ${escapeHTML(new Date(r.started_at).toLocaleTimeString())}
Spans ${spans.length}
${spansHTML}
`; }).join(''); } function getSessionSpanSummary(sp) { const payload = sp.payload || {}; const innerPayload = payload.payload || {}; if (sp.kind === 'tool') { const result = innerPayload.result_preview || ''; const duration = sp.duration_ms !== undefined && sp.duration_ms !== null ? formatDuration(sp.duration_ms) : '-'; return result ? `${duration} · ${String(result).slice(0, 80)}` : duration; } if (sp.kind === 'agent') { const usage = innerPayload.usage || {}; const totalTokens = usage.total_tokens !== undefined ? `${usage.total_tokens} tok` : ''; const duration = sp.duration_ms !== undefined && sp.duration_ms !== null ? formatDuration(sp.duration_ms) : '-'; return totalTokens ? `${duration} · ${totalTokens}` : duration; } return sp.duration_ms !== undefined && sp.duration_ms !== null ? formatDuration(sp.duration_ms) : '-'; } function bindSessionRunRows() { document.querySelectorAll('tr.expandable-run').forEach(row => { row.addEventListener('click', event => { if (event.metaKey || event.ctrlKey) { navigate('/runs/' + row.dataset.run); return; } const idx = row.dataset.index; const detailRow = document.querySelector(`tr.span-detail-row[data-index="${idx}"]`); const icon = row.querySelector('.expand-icon'); if (!detailRow) return; if (detailRow.style.display === 'none') { detailRow.style.display = 'table-row'; if (icon) icon.style.transform = 'rotate(45deg)'; } else { detailRow.style.display = 'none'; if (icon) icon.style.transform = ''; } }); row.addEventListener('dblclick', () => navigate('/runs/' + row.dataset.run)); row.setAttribute('tabindex', '0'); row.setAttribute('role', 'button'); row.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); row.click(); } }); }); } async function renderInfrastructure() { app.innerHTML = '

Loading...

'; infraUnsubscribe = subscribeWS(handleInfraWS); try { 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('/infrastructure')) { app.innerHTML = `

Error: ${escapeHTML(e.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 (eventType === 'swarm.snapshot') { mergeSwarmSnapshot(msg.data); if (isCurrentPath('/infrastructure')) renderInfraGrid(); return; } if (eventType === 'swarm.service.snapshot') { mergeSwarmServiceSnapshot(msg.data); if (isCurrentPath('/infrastructure')) renderInfraGrid(); return; } } 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; } } } function mergeSwarmSnapshot(evt) { const payload = getEnvelopePayload(evt); const services = payload.services || []; for (const svc of services) { if (svc.name) swarmState.services[svc.name] = svc; } } function mergeSwarmServiceSnapshot(evt) { const payload = getEnvelopePayload(evt); const svc = payload.service; if (svc && svc.name) swarmState.services[svc.name] = svc; } function renderInfraGrid() { const vmNames = Object.keys(openclawState.instances).sort(); const allServices = Object.values(swarmState.services); const agentmonServices = allServices.filter(s => s.group === 'agentmon'); const swarmServices = allServices.filter(s => s.group !== 'agentmon'); const homelabServices = getK8sHomelabServices(); app.innerHTML = `

VMs

${vmNames.length === 0 ? '

No VM data

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

Swarm Services

${swarmServices.length === 0 ? '

No swarm service data

' : `
${swarmServices.map(svc => renderServiceCard(svc)).join('')}
` }

K8s Homelab

${homelabServices.length === 0 ? '

No k8s homelab service data

' : `
${homelabServices.map(svc => renderHomelabServiceCard(svc)).join('')}
` }

Agentmon

${agentmonServices.length === 0 ? '

No agentmon service data

' : `
${agentmonServices.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); case 'api': case 'web': return renderAPICard(svc); case 'worker': case 'queue': return renderWorkerCard(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 renderAPICard(svc) { const httpStatus = svc.http_status; const httpClass = httpStatus === 200 ? 'ok' : httpStatus ? 'bad' : ''; return `
${serviceCardHeader(svc)}
${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 renderWorkerCard(svc) { return `
${serviceCardHeader(svc)}
${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), '')}
`; } 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; } function renderHomelabServiceCard(svc) { const httpClass = svc.httpStatus === 200 ? 'ok' : svc.httpStatus ? 'bad' : ''; return `
${serviceCardHeader(svc)}
${serviceStatRow('Endpoint', escapeHTML(svc.endpoint || '-'), '')} ${serviceStatRow('Bucket', escapeHTML(svc.bucket ? `${svc.bucket}/${svc.prefix || ''}` : '-'), '')} ${serviceStatRow('Usage', escapeHTML(formatBytes(svc.totalBytes) || '-'), '')} ${serviceStatRow('Objects', escapeHTML(svc.objectCount !== undefined ? String(svc.objectCount) : '-'), '')} ${serviceStatRow('HTTP', svc.httpStatus ? String(svc.httpStatus) : '-', httpClass)} ${serviceStatRow('Source', escapeHTML(svc.sourceInstance || '-'), '')} ${serviceStatRow('Latest', escapeHTML(svc.latestBackup ? relativeTime(svc.latestBackup) : '-'), '')} ${svc.error ? serviceStatRow('Error', escapeHTML(svc.error), 'bad') : ''}
`; } function createAgentsState() { return { agents: {}, stats: { messages: 0, tools: 0, errors: 0, toolCounts: {} }, dbStats: { messages: 0, tools: 0, errors: 0 }, viewMode: 'overview', selectedAgentKey: '', timerInterval: null, }; } 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', }; }); } function normalizeAgentKey(value) { return String(value || '') .trim() .toLowerCase() .replace(/[^a-z0-9._-]+/g, '-'); } function getAgentIdentity(evt) { const source = getEnvelopeSource(evt); const correlation = getEnvelopeCorrelation(evt); const framework = source.framework || evt.source_framework || 'unknown'; const host = source.host || ''; const clientID = source.client_id || ''; const sessionID = correlation.session_id || ''; const name = clientID || host || framework || sessionID || 'unknown'; const key = normalizeAgentKey(clientID || host || sessionID || framework || 'unknown'); return { key, name, framework, host, clientID, sessionID, }; } function ensureAgentBucket(evt) { const identity = getAgentIdentity(evt); if (!identity.key) { return null; } if (!agentsState.agents[identity.key]) { agentsState.agents[identity.key] = { key: identity.key, name: identity.name, framework: identity.framework, host: identity.host, clientID: identity.clientID, sessions: {}, operations: {}, events: [], eventIDs: new Set(), lastSeenAt: 0, liveLoaded: false, liveLoading: false, }; } const agent = agentsState.agents[identity.key]; agent.name = identity.name || agent.name || identity.key; agent.framework = identity.framework || agent.framework; agent.host = identity.host || agent.host; agent.clientID = identity.clientID || agent.clientID; return agent; } function getSortedAgentKeys() { return Object.keys(agentsState.agents).sort((a, b) => { const left = agentsState.agents[a]; const right = agentsState.agents[b]; const leftOnline = isAgentOnline(left); const rightOnline = isAgentOnline(right); if (leftOnline !== rightOnline) { return leftOnline ? -1 : 1; } return (left.name || left.key).localeCompare(right.name || right.key); }); } function ensureSelectedAgentKey() { const keys = getSortedAgentKeys(); if (keys.length === 0) { agentsState.selectedAgentKey = ''; return ''; } if (!agentsState.selectedAgentKey || !agentsState.agents[agentsState.selectedAgentKey]) { agentsState.selectedAgentKey = keys[0]; } return agentsState.selectedAgentKey; } function setAgentsViewMode(mode) { agentsState.viewMode = mode === 'live' ? 'live' : 'overview'; renderAgentsContent(); if (agentsState.viewMode === 'live') { void loadSelectedAgentLiveData(); } } function selectAgent(key, nextMode) { if (!key || !agentsState.agents[key]) return; agentsState.selectedAgentKey = key; if (nextMode) { agentsState.viewMode = nextMode; } renderAgentsContent(); if (agentsState.viewMode === 'live') { void loadSelectedAgentLiveData(); } } function isOpenClawVM(agent) { const key = normalizeAgentKey(agent && agent.name); return !!openclawState.instances[key]; } 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; } function getAgentBucket(evt) { return ensureAgentBucket(evt); } function processAgentEvent(evt) { const agent = getAgentBucket(evt); if (!agent) return; const eventType = getEnvelopeType(evt); const correlation = getEnvelopeCorrelation(evt); const attrs = getEnvelopeAttributes(evt); const ts = new Date(getEnvelopeTS(evt)).getTime(); agent.lastSeenAt = Number.isFinite(ts) ? ts : Date.now(); if (eventType === 'session.start' && correlation.session_id) { agent.sessions[correlation.session_id] = { ts: getEnvelopeTS(evt) }; } if (eventType === 'session.end' && correlation.session_id) { delete agent.sessions[correlation.session_id]; } if (eventType === 'span.start' && correlation.span_id) { const payload = getEnvelopePayload(evt); agent.operations['s:' + correlation.span_id] = { type: 'span', name: attrs.name || attrs.span_kind || 'unknown', kind: attrs.span_kind || '', subType: attrs.type || '', startedAt: new Date(getEnvelopeTS(evt)).getTime() || Date.now(), promptPreview: payload.prompt_preview || '', inputPreview: payload.input ? (typeof payload.input === 'string' ? payload.input : JSON.stringify(payload.input)) : '', spanID: correlation.span_id, runID: correlation.run_id || '', }; } if (eventType === 'span.end' && correlation.span_id) { const op = agent.operations['s:' + correlation.span_id]; if (op) { const payload = getEnvelopePayload(evt); op.resultPreview = payload.result_preview || ''; op.status = payload.status || ''; op.durationMS = payload.duration_ms || 0; op.endedAt = new Date(getEnvelopeTS(evt)).getTime() || Date.now(); op.usage = payload.usage || null; // Keep completed ops briefly for display, then remove and refresh stream setTimeout(() => { delete agent.operations['s:' + correlation.span_id]; refreshThinkingStream(agent); }, 3000); } } if (eventType === 'run.start' && correlation.run_id) { const payload = getEnvelopePayload(evt); agent.operations['r:' + correlation.run_id] = { type: 'run', name: 'Thinking…', kind: 'run', startedAt: new Date(getEnvelopeTS(evt)).getTime() || Date.now(), promptPreview: payload.prompt_preview || payload.message_preview || payload.message || '', runID: correlation.run_id, }; } if (eventType === 'run.end' && correlation.run_id) { const op = agent.operations['r:' + correlation.run_id]; if (op) { const payload = getEnvelopePayload(evt); op.endedAt = new Date(getEnvelopeTS(evt)).getTime() || Date.now(); op.status = payload.status || ''; op.usage = payload.usage || null; op.model = payload.model || ''; op.thinkingTokens = (payload.usage && payload.usage.thinking_tokens) || 0; setTimeout(() => { delete agent.operations['r:' + correlation.run_id]; refreshThinkingStream(agent); }, 2000); } } const id = getRecordID(evt); if (id && !agent.eventIDs.has(id)) { agent.eventIDs.add(id); agent.events.push(evt); while (agent.events.length > 100) { const removed = agent.events.shift(); agent.eventIDs.delete(getRecordID(removed)); } } } function getAgentDisplayOps(agent) { const now = Date.now(); const ops = Object.values(agent.operations).filter(op => (now - op.startedAt) < 300000); const hasSpecificSpans = ops.some(op => op.kind && op.kind !== 'run'); return hasSpecificSpans ? ops.filter(op => op.kind && op.kind !== 'run') : ops; } function isAgentTimelineEvent(evt) { const eventType = getEnvelopeType(evt); return [ 'session.start', 'session.end', 'run.start', 'run.end', 'span.start', 'span.end', 'error', ].includes(eventType); } function isDashboardFeedEvent(evt) { const eventType = getEnvelopeType(evt); return isAgentTimelineEvent(evt) || eventType === 'metric.snapshot'; } 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'}`, }; } async function renderAgents() { agentsState = createAgentsState(); app.innerHTML = `

Loading...

`; bindAgentViewToggle(); try { const [snapshots, events, summaryData] = await Promise.all([ api('/v1/events?event_type=openclaw.snapshot&limit=100').catch(() => ({ events: [] })), api('/v1/events?limit=300'), api('/v1/stats/summary').catch(() => null), ]); if (!isCurrentPath('/agents')) return; if (summaryData) { agentsState.dbStats.messages = summaryData.runs_today || 0; agentsState.dbStats.tools = summaryData.tool_calls_today || 0; agentsState.dbStats.errors = summaryData.errors_today || 0; } mergeOpenClawEvents(snapshots.events || []); addAgentEvents((events.events || []).filter(isAgentTimelineEvent).slice().reverse()); renderAgentsContent(); } catch (e) { document.getElementById('agents-content').innerHTML = `

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

`; } agentsState.timerInterval = setInterval(updateAgentTimers, 1000); agentsUnsubscribe = subscribeWS(handleAgentsWS); } function bindAgentViewToggle() { const root = document.getElementById('agents-view-toggle'); if (!root) return; root.querySelectorAll('[data-mode]').forEach(button => { button.addEventListener('click', () => { setAgentsViewMode(button.dataset.mode || 'overview'); }); }); } function updateAgentViewToggle() { const root = document.getElementById('agents-view-toggle'); if (!root) return; root.querySelectorAll('[data-mode]').forEach(button => { button.classList.toggle('active', button.dataset.mode === agentsState.viewMode); }); } function renderAgentsContent() { renderAgentSummary(); updateAgentViewToggle(); if (agentsState.viewMode === 'live') { renderAgentsLive(); return; } renderAgentLanes(); } async function loadSelectedAgentLiveData() { const selectedKey = ensureSelectedAgentKey(); if (!selectedKey) return; const agent = agentsState.agents[selectedKey]; if (!agent || agent.liveLoaded || agent.liveLoading || !agent.clientID || !agent.framework) { return; } agent.liveLoading = true; try { const params = new URLSearchParams(); params.set('client_id', agent.clientID); params.set('framework', agent.framework); params.set('limit', '250'); const data = await api('/v1/agents/live?' + params.toString()); addAgentEvents((data.events || []).slice().reverse()); agent.liveLoaded = true; } catch (err) { console.error('Failed to load live agent context:', err); } finally { agent.liveLoading = false; if (isCurrentPath('/agents') && agentsState.viewMode === 'live' && agentsState.selectedAgentKey === selectedKey) { renderAgentsContent(); } } } function renderAgentLanes() { const contentEl = document.getElementById('agents-content'); if (!contentEl) return; contentEl.innerHTML = '
'; const lanesEl = document.getElementById('agents-lanes'); if (!lanesEl) return; const agentKeys = getSortedAgentKeys(); if (agentKeys.length === 0) { lanesEl.innerHTML = '

No recent agent activity

'; return; } lanesEl.innerHTML = agentKeys.map(key => { const agent = agentsState.agents[key]; const isOnline = isAgentOnline(agent); const sessionCount = Object.keys(agent.sessions).length; const ops = getAgentDisplayOps(agent); const subagentCount = ops.filter(op => op.kind === 'agent' || op.subType === 'subagent').length; const statusClass = sessionCount > 0 ? ' has-sessions' : ''; const statusText = !isOnline ? 'offline' : subagentCount > 0 ? subagentCount + ' subagent' + (subagentCount > 1 ? 's' : '') : sessionCount > 0 ? sessionCount + ' session' + (sessionCount > 1 ? 's' : '') : 'idle'; const opsHTML = ops.length > 0 ? `
${ops.map(op => { const elapsed = Math.floor((Date.now() - op.startedAt) / 1000); const stale = elapsed > 300; const kindClass = op.kind === 'agent' || op.subType === 'subagent' ? ' subagent' : ''; return `
${escapeHTML(op.name)} ${formatElapsed(elapsed)} ${stale ? '(stale?)' : ''}
`; }).join('')}
` : ''; const recent = agent.events.slice(-40).reverse(); const eventsHTML = recent.length > 0 ? recent.map(evt => { const eventType = getEnvelopeType(evt); const details = getEventDetails(evt); const detailHTML = details ? `
${escapeHTML(details)}
` : ''; const expandHTML = details ? '' : ''; return `
${getEventIcon(eventType)} ${escapeHTML(getEventLabel(eventType))} ${escapeHTML(new Date(getEnvelopeTS(evt)).toLocaleTimeString())}
${getEventBody(evt)} ${expandHTML} ${detailHTML}
`; }).join('') : '

No recent activity

'; return `
${escapeHTML(agent.name || key)}
${escapeHTML(agent.framework || 'unknown')}${agent.host && agent.host !== agent.name ? ' · ' + escapeHTML(agent.host) : ''}
${statusText}
${opsHTML}
${eventsHTML}
`; }).join(''); lanesEl.querySelectorAll('.agent-lane[data-agent-key]').forEach(lane => { lane.addEventListener('click', () => { selectAgent(lane.dataset.agentKey || '', 'live'); }); }); lanesEl.querySelectorAll('.timeline-expand-hint').forEach(button => { button.addEventListener('click', event => { event.stopPropagation(); button.parentElement.classList.toggle('expanded'); }); }); } function formatElapsed(seconds) { if (seconds < 60) return seconds + 's'; if (seconds < 3600) return Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's'; return Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm'; } function renderAgentSummary() { const el = document.getElementById('agents-summary'); if (!el) return; const s = agentsState.dbStats; const liveAgents = getSortedAgentKeys().filter(key => isAgentOnline(agentsState.agents[key])).length; const liveSubagents = getSortedAgentKeys().reduce((count, key) => { const agent = agentsState.agents[key]; return count + Object.values(agent.operations).filter(op => op.kind === 'agent' || op.subType === 'subagent').length; }, 0); el.innerHTML = `
Live Agents ${liveAgents}
Active Subagents ${liveSubagents}
Runs Today ${s.messages}
Tool Calls ${s.tools}
Errors ${s.errors}
`; } function getAgentLabel(agent) { if (!agent) return 'Unknown'; return agent.name || agent.host || agent.framework || agent.key || 'Unknown'; } function getAgentLiveSummary(agent) { const recent = agent.events.slice().reverse(); const activeOps = getAgentDisplayOps(agent); const sessionIDs = Object.keys(agent.sessions); const live = { sessionIDs, activeOps, activeSubagents: activeOps.filter(op => op.kind === 'agent' || op.subType === 'subagent'), activeTools: activeOps.filter(op => op.kind === 'tool'), latestPrompt: '', latestRunStatus: '', latestModel: '', latestError: '', latestUsage: null, latestContextWindow: null, }; for (const evt of recent) { const eventType = getEnvelopeType(evt); const payload = getEnvelopePayload(evt); if (!live.latestPrompt && eventType === 'run.start') { live.latestPrompt = payload.prompt_preview || payload.message_preview || payload.message || ''; } if (!live.latestRunStatus && eventType === 'run.end') { live.latestRunStatus = payload.status || ''; live.latestModel = payload.model || ''; live.latestUsage = payload.usage || null; live.latestContextWindow = payload.context_window || null; } if (!live.latestUsage && eventType === 'metric.snapshot' && payload.metrics) { live.latestUsage = payload.metrics.usage || null; live.latestModel = live.latestModel || payload.metrics.model || ''; } if (!live.latestError && eventType === 'error') { const errPayload = payload.error || {}; live.latestError = errPayload.message || payload.message || ''; } if (live.latestPrompt && live.latestRunStatus && live.latestError) { break; } } return live; } function formatCount(value) { if (value === undefined || value === null || value === '') return '-'; return String(value); } function formatCost(value) { if (value === undefined || value === null || value === '') return '-'; const num = Number(value); if (!Number.isFinite(num)) return String(value); return '$' + num.toFixed(4); } function formatTokenCount(value) { if (value === undefined || value === null || value === '') return '-'; const n = Number(value); if (!Number.isFinite(n)) return String(value); if (n === 0) return '0'; if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M'; if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K'; return String(n); } function buildLiveEventContext(evt) { const eventType = getEnvelopeType(evt); const payload = getEnvelopePayload(evt); const attrs = getEnvelopeAttributes(evt); const correlation = getEnvelopeCorrelation(evt); const parts = []; if ((eventType === 'span.start' || eventType === 'span.end') && attrs.span_kind === 'tool') { if (payload.input) { parts.push(`
input${escapeHTML(typeof payload.input === 'string' ? payload.input : JSON.stringify(payload.input))}
`); } if (payload.result_preview) { parts.push(`
result${escapeHTML(String(payload.result_preview))}
`); } } if ((eventType === 'span.start' || eventType === 'span.end') && (attrs.span_kind === 'agent' || attrs.type === 'subagent')) { if (payload.prompt_preview) { parts.push(`
prompt${escapeHTML(String(payload.prompt_preview))}
`); } if (payload.usage && payload.usage.total_tokens !== undefined) { parts.push(`
tokens${escapeHTML(formatCount(payload.usage.total_tokens))}
`); } if (payload.usage && payload.usage.total_cost !== undefined) { parts.push(`
cost${escapeHTML(formatCost(payload.usage.total_cost))}
`); } } if (eventType === 'run.start') { const preview = payload.prompt_preview || payload.message_preview || payload.message || ''; if (preview) { parts.push(`
prompt${escapeHTML(String(preview))}
`); } } if (eventType === 'run.end') { if (payload.model) { parts.push(`
model${escapeHTML(String(payload.model))}
`); } if (payload.usage && payload.usage.total_tokens !== undefined) { parts.push(`
tokens${escapeHTML(formatCount(payload.usage.total_tokens))}
`); } if (payload.duration_ms !== undefined) { parts.push(`
duration${escapeHTML(formatDuration(payload.duration_ms))}
`); } } if (eventType === 'metric.snapshot' && payload.metrics) { if (payload.metrics.model) { parts.push(`
model${escapeHTML(String(payload.metrics.model))}
`); } if (payload.metrics.usage && payload.metrics.usage.total_tokens !== undefined) { parts.push(`
tokens${escapeHTML(formatCount(payload.metrics.usage.total_tokens))}
`); } if (payload.metrics.usage && payload.metrics.usage.total_cost !== undefined) { parts.push(`
cost${escapeHTML(formatCost(payload.metrics.usage.total_cost))}
`); } } if (eventType === 'error') { const errPayload = payload.error || {}; if (errPayload.type) { parts.push(`
type${escapeHTML(String(errPayload.type))}
`); } } const ids = []; if (correlation.session_id) ids.push(`session ${correlation.session_id}`); if (correlation.run_id) ids.push(`run ${correlation.run_id}`); if (correlation.span_id) ids.push(`span ${correlation.span_id}`); if (ids.length > 0) { parts.push(`
ids${escapeHTML(ids.join(' · '))}
`); } return parts.join(''); } function getRunGroupLabel(runID, events) { const runStart = events.find(evt => getEnvelopeType(evt) === 'run.start'); if (!runStart) { return runID ? `Run ${runID.slice(0, 12)}...` : 'Session activity'; } const payload = getEnvelopePayload(runStart); const preview = payload.prompt_preview || payload.message_preview || payload.message || ''; if (preview) { return preview.length > 72 ? preview.slice(0, 72) + '...' : preview; } return runID ? `Run ${runID.slice(0, 12)}...` : 'Session activity'; } function groupAgentEventsByRun(events) { const groups = []; const byRun = new Map(); for (const evt of events) { const correlation = getEnvelopeCorrelation(evt); const runID = correlation.run_id || ''; const key = runID || `session:${correlation.session_id || 'unknown'}`; if (!byRun.has(key)) { const group = { key, runID, sessionID: correlation.session_id || '', events: [], subagents: new Set(), tools: new Set(), }; byRun.set(key, group); groups.push(group); } const group = byRun.get(key); group.events.push(evt); const attrs = getEnvelopeAttributes(evt); if (attrs.span_kind === 'agent' || attrs.type === 'subagent') { group.subagents.add(attrs.name || 'unknown'); } if (attrs.span_kind === 'tool' && attrs.name) { group.tools.add(attrs.name); } } return groups; } function refreshThinkingStream(agent) { if (!agent) return; const selectedKey = agentsState.selectedAgentKey; if (agent.key !== selectedKey) return; const streamEl = document.getElementById('thinking-stream-' + selectedKey); if (streamEl) { streamEl.innerHTML = renderThinkingStream(agent); } } function renderThinkingStream(agent) { const now = Date.now(); const ops = Object.values(agent.operations).filter(op => { // Show ended ops only if recently ended (within 3s) if (op.endedAt) return (now - op.endedAt) < 3000; return (now - op.startedAt) < 300000; }); if (ops.length === 0) { return '
Idle — waiting for activity
'; } return ops.map(op => { const elapsed = op.endedAt ? Math.floor((op.endedAt - op.startedAt) / 1000) : Math.floor((now - op.startedAt) / 1000); const isEnded = !!op.endedAt; const isSubagent = op.kind === 'agent' || op.subType === 'subagent'; const isRun = op.kind === 'run'; const isTool = op.kind === 'tool'; let icon, kindLabel, kindClass; if (isRun) { icon = isEnded ? '✓' : '◌'; kindLabel = isEnded ? (op.status === 'success' ? 'Done' : op.status || 'Done') : 'Thinking'; kindClass = 'thinking-op-run' + (isEnded ? ' ended' : ' active'); } else if (isSubagent) { icon = isEnded ? '✓' : '◎'; kindLabel = isEnded ? (op.status === 'success' ? 'Subagent done' : 'Subagent ' + (op.status || 'done')) : 'Subagent'; kindClass = 'thinking-op-subagent' + (isEnded ? ' ended' : ' active'); } else if (isTool) { icon = isEnded ? '✓' : '▸'; kindLabel = isEnded ? (op.status === 'success' ? 'Tool done' : 'Tool ' + (op.status || 'done')) : 'Tool'; kindClass = 'thinking-op-tool' + (isEnded ? ' ended' : ' active'); } else { icon = '·'; kindLabel = op.name; kindClass = 'thinking-op-other' + (isEnded ? ' ended' : ' active'); } const preview = op.promptPreview || op.inputPreview || ''; const result = op.resultPreview || ''; const usage = op.usage || {}; const thinkingToks = op.thinkingTokens || usage.thinking_tokens || 0; const totalToks = usage.total_tokens || 0; const navigableRunID = isRun ? op.runID : (isSubagent ? op.runID : ''); const clickable = navigableRunID ? ` clickable" data-run-id="${escapeHTML(navigableRunID)}` : ''; return `
${icon} ${escapeHTML(kindLabel)} ${escapeHTML(op.name)} ${isEnded ? formatElapsed(elapsed) : `${formatElapsed(elapsed)}`} ${navigableRunID ? '' : ''}
${preview ? `
${escapeHTML(preview.length > 180 ? preview.slice(0, 180) + '…' : preview)}
` : ''} ${result ? `
${escapeHTML(result.length > 180 ? result.slice(0, 180) + '…' : result)}
` : ''} ${(thinkingToks || totalToks) ? `
${thinkingToks ? `🧠 ${formatTokenCount(thinkingToks)} thinking` : ''}${totalToks ? `⚡ ${formatTokenCount(totalToks)} total` : ''}
` : ''}
`; }).join(''); } function renderAgentsLive() { const contentEl = document.getElementById('agents-content'); if (!contentEl) return; const agentKeys = getSortedAgentKeys(); const selectedKey = ensureSelectedAgentKey(); if (!selectedKey || agentKeys.length === 0) { contentEl.innerHTML = '

No recent agent activity

'; return; } const selected = agentsState.agents[selectedKey]; const summary = getAgentLiveSummary(selected); const recent = selected.events.slice(-80).reverse(); const runGroups = groupAgentEventsByRun(recent); contentEl.innerHTML = `
${escapeHTML(getAgentLabel(selected))}
${escapeHTML(selected.framework || 'unknown')}${selected.host && selected.host !== selected.name ? ' · ' + escapeHTML(selected.host) : ''}
${summary.sessionIDs.length} sessions ${summary.activeSubagents.length} subagents ${summary.activeTools.length} tools
Live Operations ${summary.activeOps.length > 0 ? `${summary.activeOps.length} active` : ''}
${renderThinkingStream(selected)}
Last Run
Status${escapeHTML(summary.latestRunStatus || '—')}
Model${escapeHTML(summary.latestModel || '—')}
Tokens${escapeHTML(formatTokenCount(summary.latestUsage ? summary.latestUsage.total_tokens : null))}
Thinking${escapeHTML(formatTokenCount(summary.latestUsage ? summary.latestUsage.thinking_tokens : null))}
Cost${escapeHTML(formatCost(summary.latestUsage ? summary.latestUsage.total_cost : null))}
${summary.latestError ? `
Error${escapeHTML(summary.latestError)}
` : ''}
Context Window
Input${escapeHTML(formatTokenCount(summary.latestContextWindow ? summary.latestContextWindow.input_tokens : null))}
Output${escapeHTML(formatTokenCount(summary.latestContextWindow ? summary.latestContextWindow.output_tokens : null))}
Used${escapeHTML(formatTokenCount(summary.latestContextWindow ? summary.latestContextWindow.used_tokens : null))}
Remaining${escapeHTML(formatTokenCount(summary.latestContextWindow ? summary.latestContextWindow.tokens_remaining : null))}
${summary.latestContextWindow && summary.latestContextWindow.max_tokens ? `
` : ''}
${runGroups.length > 0 ? runGroups.map(group => `
${escapeHTML(getRunGroupLabel(group.runID, group.events))}
${escapeHTML(group.runID ? `run ${group.runID.slice(0, 12)}...` : 'session-only')} ${escapeHTML(group.subagents.size > 0 ? `${group.subagents.size} subagents` : '0 subagents')} ${escapeHTML(group.tools.size > 0 ? `${group.tools.size} tools` : '0 tools')}
${group.events.map(evt => `
${getEventIcon(getEnvelopeType(evt))} ${escapeHTML(getEventLabel(getEnvelopeType(evt)))} ${escapeHTML(new Date(getEnvelopeTS(evt)).toLocaleTimeString())}
${getEventBody(evt)}
${buildLiveEventContext(evt)}
`).join('')}
`).join('') : '

No recent activity

'}
`; contentEl.querySelectorAll('[data-agent-key]').forEach(button => { button.addEventListener('click', () => selectAgent(button.dataset.agentKey || '', 'live')); }); // Delegate thinking-op clicks — bound once on stable container, survives 1s stream refresh const mainSection = contentEl.querySelector('.agents-live-main'); if (mainSection) { mainSection.addEventListener('click', e => { const op = e.target.closest('.thinking-op[data-run-id]'); if (op && op.dataset.runId) { navigate('/runs/' + op.dataset.runId); } }); } } function renderAgentVMStrip() { // VM online/offline state is shown in each lane header via getVMStatus(). // Re-render lanes to pick up the updated openclawState. renderAgentsContent(); } let _agentsRenderTimer = null; function scheduleAgentsRender() { if (_agentsRenderTimer) return; _agentsRenderTimer = requestAnimationFrame(() => { _agentsRenderTimer = null; renderAgentsContent(); }); } function handleAgentsWS(msg) { if (msg.type !== 'message') return; const eventType = getEnvelopeType(msg.data); if (eventType === 'openclaw.snapshot') { mergeOpenClawEvents([msg.data]); scheduleAgentsRender(); return; } if (!isAgentTimelineEvent(msg.data)) return; if (eventType === 'run.start') agentsState.dbStats.messages++; else if (eventType === 'span.end') { const attrs = getEnvelopeAttributes(msg.data); if (attrs.span_kind === 'tool') agentsState.dbStats.tools++; } else if (eventType === 'error') agentsState.dbStats.errors++; addAgentEvents([msg.data]); scheduleAgentsRender(); } function updateAgentTimers() { document.querySelectorAll('.active-op-time[data-start]').forEach(el => { const start = parseInt(el.dataset.start, 10); if (!start) return; const elapsed = Math.floor((Date.now() - start) / 1000); el.textContent = formatElapsed(elapsed); const op = el.closest('.active-op'); if (op && elapsed > 300 && !op.classList.contains('stale')) { op.classList.add('stale'); if (!op.querySelector('.active-op-stale')) { op.insertAdjacentHTML('beforeend', '(stale?)'); } } }); } function addAgentEvents(events) { let changed = false; for (const evt of events) { const id = getRecordID(evt); const agent = getAgentBucket(evt); if (!id || !agent || agent.eventIDs.has(id)) continue; processAgentEvent(evt); changed = true; } if (changed) { for (const agent of Object.values(agentsState.agents)) { agent.events.sort((a, b) => new Date(getEnvelopeTS(a)).getTime() - new Date(getEnvelopeTS(b)).getTime()); } recomputeAgentStats(); } } function recomputeAgentStats() { const stats = { messages: 0, tools: 0, errors: 0, toolCounts: {} }; for (const agent of Object.values(agentsState.agents)) { for (const evt of agent.events) { const eventType = getEnvelopeType(evt); const attrs = getEnvelopeAttributes(evt); if (eventType === 'run.start' || eventType === 'run.end') stats.messages++; if (eventType === 'span.end' && attrs.span_kind === 'tool') { stats.tools++; const toolName = attrs.name || 'unknown'; stats.toolCounts[toolName] = (stats.toolCounts[toolName] || 0) + 1; } if (eventType === 'error') stats.errors++; } } agentsState.stats = stats; } function getEventIcon(eventType) { switch (eventType) { case 'run.start': return '
'; case 'run.end': return '
'; case 'span.start': case 'span.end': return '
'; case 'error': return '
!
'; case 'session.start': case 'session.end': return '
'; default: return '
·
'; } } function getEventLabel(eventType) { const labels = { 'session.start': 'Session Started', 'session.end': 'Session Ended', 'run.start': 'Message Received', 'run.end': 'Response Sent', 'span.start': 'Span Started', 'span.end': 'Span Completed', 'error': 'Error', 'metric.snapshot': 'Metric', }; return labels[eventType] || eventType; } function getVMName(evt) { return getAgentIdentity(evt).name || 'unknown'; } function getVMClassName(vmName) { const normalized = String(vmName || 'unknown').toLowerCase(); return ['zap', 'orb', 'sun'].includes(normalized) ? normalized : 'unknown'; } function getEventBody(evt) { const eventType = getEnvelopeType(evt); const payload = getEnvelopePayload(evt); const attrs = getEnvelopeAttributes(evt); const correlation = getEnvelopeCorrelation(evt); if (eventType === 'span.start' || eventType === 'span.end') { const name = attrs.name || attrs.span_kind || 'unknown span'; const duration = payload.duration_ms !== undefined && payload.duration_ms !== null ? ` ${escapeHTML(formatDuration(payload.duration_ms))}` : ''; const detailClass = attrs.span_kind === 'agent' || attrs.type === 'subagent' ? ' subagent-name' : ' tool-name'; const prefix = attrs.span_kind === 'agent' || attrs.type === 'subagent' ? 'subagent ' : ''; return `
${escapeHTML(prefix + name)}${duration}
`; } if (eventType === 'run.start') { const preview = payload.prompt_preview || payload.message_preview || payload.message || ''; if (!preview) { return ''; } const trimmed = preview.length > 140 ? preview.slice(0, 140) + '...' : preview; return `
"${escapeHTML(trimmed)}"
`; } if (eventType === 'run.end') { return `
${statusIcon(payload.status || 'unknown')}
`; } if (eventType === 'error') { const errPayload = payload.error || {}; const errType = errPayload.type || 'error'; const message = errPayload.message || payload.message || 'unknown'; return `
${escapeHTML(errType + ': ' + message)}
`; } if (eventType === 'session.start' || eventType === 'session.end') { return correlation.session_id ? `
session ${escapeHTML(correlation.session_id)}
` : ''; } return ''; } function getEventDetails(evt) { const details = {}; const correlation = getEnvelopeCorrelation(evt); const attributes = getEnvelopeAttributes(evt); const payload = getEnvelopePayload(evt); if (Object.keys(correlation).length > 0) { details.correlation = correlation; } if (Object.keys(attributes).length > 0) { details.attributes = attributes; } if (Object.keys(payload).length > 0) { details.payload = payload; } if (Object.keys(details).length === 0) { return ''; } return JSON.stringify(details, null, 2); } function persistDashboardRecentEvents() { if (!dashboardState) return; localStorage.setItem( DASH_RECENT_EVENTS_STORAGE_KEY, JSON.stringify(dashboardState.recentEvents.slice(-DASH_RECENT_EVENTS_LIMIT)), ); } function addDashboardRecentEvent(evt) { if (!dashboardState || !isDashboardFeedEvent(evt)) { return false; } const id = getRecordID(evt); if (id && dashboardState.recentEventIDs.has(id)) { return false; } if (id) { dashboardState.recentEventIDs.add(id); } dashboardState.recentEvents.push(evt); while (dashboardState.recentEvents.length > DASH_RECENT_EVENTS_LIMIT) { const removed = dashboardState.recentEvents.shift(); const removedID = getRecordID(removed); if (removedID) { dashboardState.recentEventIDs.delete(removedID); } } persistDashboardRecentEvents(); return true; } async function renderDashboard() { dashboardState = { summary: null, timeseries: null, window: '1h', chartMode: getDashboardChartMode(), chartCursorIndex: null, recentEvents: [], recentEventIDs: new Set(), toolCounts: {}, modelCounts: {}, rightPanelMode: localStorage.getItem('agentmon:dash:right-panel') || 'framework', }; app.innerHTML = `
Active Sessions
-
 
Runs Today
-
 
Tool Calls
-
 
Errors
-
 
Tokens today -
Cost today -
Avg run duration -
Error rate -
Infrastructure
Event Rate Runs, tool spans, and errors over time
total runs tools errors

Loading...

Recent Activity

Loading...

Top Usage
Tools
  • Loading...
Models
  • Loading...
`; document.querySelectorAll('.window-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.window-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); dashboardState.window = btn.dataset.w; loadTimeseries(); }); }); document.querySelectorAll('.mode-btn').forEach(btn => { btn.addEventListener('click', () => { const nextMode = btn.dataset.mode; if (dashboardState.chartMode === nextMode) return; document.querySelectorAll('.mode-btn').forEach(b => b.classList.toggle('active', b === btn)); dashboardState.chartMode = nextMode; localStorage.setItem('agentmon:dash:chart-mode', nextMode); if (dashboardChart) { dashboardChart.destroy(); dashboardChart = null; } renderTimeseriesChart(); }); }); document.querySelectorAll('.right-panel-tab').forEach(btn => { btn.addEventListener('click', () => { const panel = btn.dataset.panel; if (dashboardState.rightPanelMode === panel) return; document.querySelectorAll('.right-panel-tab').forEach(b => b.classList.toggle('active', b === btn)); dashboardState.rightPanelMode = panel; localStorage.setItem('agentmon:dash:right-panel', panel); renderRightPanel(); }); }); renderDashVMStrip(); const cachedRecentEvents = tryParseJSON(localStorage.getItem(DASH_RECENT_EVENTS_STORAGE_KEY)); if (Array.isArray(cachedRecentEvents)) { for (const evt of cachedRecentEvents) { addDashboardRecentEvent(evt); } renderDashFeed(); } // Render cached data immediately while the API call is in-flight const cachedSummary = tryParseJSON(localStorage.getItem('agentmon:dash:summary')); const cachedTS = tryParseJSON(localStorage.getItem('agentmon:dash:ts:' + dashboardState.window)); if (cachedSummary) { dashboardState.summary = cachedSummary; renderSummaryCards(); } if (cachedTS) { dashboardState.timeseries = cachedTS; renderTimeseriesChart(); renderRightPanel(); } try { const [summaryData, tsData, recentData, snapshots, swarmSnaps, topToolsData, topModelsData] = await Promise.all([ api('/v1/stats/summary'), api('/v1/stats/timeseries?window=1h'), api('/v1/events?limit=10'), api('/v1/events?event_type=openclaw.snapshot&limit=100').catch(() => ({ events: [] })), api('/v1/events?event_type=swarm.snapshot&limit=10').catch(() => ({ events: [] })), api('/v1/stats/top-tools').catch(() => ({ tools: [] })), api('/v1/stats/top-models').catch(() => ({ models: [] })), ]); if (!isCurrentPath('/')) return; mergeOpenClawEvents(snapshots.events || []); for (const evt of swarmSnaps.events || []) mergeSwarmSnapshot(evt); renderDashVMStrip(); dashboardState.summary = summaryData; dashboardState.timeseries = tsData; localStorage.setItem('agentmon:dash:summary', JSON.stringify(summaryData)); localStorage.setItem('agentmon:dash:ts:' + dashboardState.window, JSON.stringify(tsData)); renderSummaryCards(); renderTimeseriesChart(); renderRightPanel(); // Seed tool counts from the dedicated top-tools endpoint for (const t of (topToolsData.tools || [])) { dashboardState.toolCounts[t.name] = t.count; } for (const m of (topModelsData.models || [])) { dashboardState.modelCounts[m.name] = m.count; } const events = (recentData.events || []) .filter(isDashboardFeedEvent) .slice() .reverse(); for (const evt of events) { addDashboardRecentEvent(evt); } renderDashFeed(); renderDashTopTools(); renderDashTopModels(); } catch (e) { console.error('Dashboard load error:', e); } dashboardUnsubscribe = subscribeWS(handleDashboardWS); } function renderDashVMStrip() { const strip = document.getElementById('dash-vm-strip'); if (!strip) return; const vms = getVMStatus(); const infra = getDashboardInfraPill(); strip.innerHTML = [ ...vms.map(vm => `
${escapeHTML(vm.name)} ${vm.active ? 'online' : 'offline'}
`), `
${escapeHTML(infra.name)} ${escapeHTML(infra.label)}
`, ].join(''); } function handleDashboardWS(msg) { if (msg.type !== 'message') return; const eventType = getEnvelopeType(msg.data); if (eventType === 'openclaw.snapshot') { mergeOpenClawEvents([msg.data]); renderDashVMStrip(); return; } if (eventType === 'swarm.snapshot') { mergeSwarmSnapshot(msg.data); renderDashVMStrip(); return; } if (eventType === 'swarm.service.snapshot') { mergeSwarmServiceSnapshot(msg.data); renderDashVMStrip(); return; } if (dashboardState.summary) { if (eventType === 'session.start') dashboardState.summary.active_sessions++; if (eventType === 'session.end') dashboardState.summary.active_sessions = Math.max(0, dashboardState.summary.active_sessions - 1); if (eventType === 'run.start') dashboardState.summary.runs_today++; if (eventType === 'error') dashboardState.summary.errors_today++; if (eventType === 'span.end') { const attrs = getEnvelopeAttributes(msg.data); if (attrs.span_kind === 'tool') dashboardState.summary.tool_calls_today++; } if (eventType === 'run.end') { const payload = getEnvelopePayload(msg.data); const usage = payload.usage || {}; dashboardState.summary.tokens_today = (dashboardState.summary.tokens_today || 0) + (usage.total_tokens || 0); dashboardState.summary.cost_today = (dashboardState.summary.cost_today || 0) + (usage.total_cost || 0); // Update rolling avg duration if (payload.duration_ms) { const runs = dashboardState.summary.runs_today || 1; const prev = dashboardState.summary.avg_duration_ms || 0; dashboardState.summary.avg_duration_ms = prev + (payload.duration_ms - prev) / runs; } } renderSummaryCards(); } if (!isDashboardFeedEvent(msg.data)) { if (dashboardState.timeseries && dashboardState.window === '1h') { appendToCurrentBucket(msg.data); } return; } if (addDashboardRecentEvent(msg.data)) { tallyTool(msg.data); tallyModel(msg.data); if (!_dashFeedRenderTimer) { _dashFeedRenderTimer = requestAnimationFrame(() => { _dashFeedRenderTimer = null; renderDashFeed(); renderDashTopTools(); renderDashTopModels(); }); } } if (dashboardState.timeseries && dashboardState.window === '1h') { appendToCurrentBucket(msg.data); } } function tallyTool(evt) { const eventType = getEnvelopeType(evt); if (eventType === 'span.end') { const attrs = getEnvelopeAttributes(evt); if (attrs.span_kind === 'tool') { const name = attrs.name || 'unknown'; dashboardState.toolCounts[name] = (dashboardState.toolCounts[name] || 0) + 1; } } } function tallyModel(evt) { const eventType = getEnvelopeType(evt); const payload = getEnvelopePayload(evt); if (eventType === 'run.end' && payload.model) { const name = String(payload.model); dashboardState.modelCounts[name] = (dashboardState.modelCounts[name] || 0) + 1; return; } if (eventType === 'metric.snapshot' && payload.metrics && payload.metrics.model) { const name = String(payload.metrics.model); if (!dashboardState.modelCounts[name]) { dashboardState.modelCounts[name] = 1; } } } function renderSummaryCards() { const s = dashboardState.summary; if (!s) return; const el = (id, val) => { const e = document.getElementById(id); if (e) e.textContent = String(val); }; el('dash-active', s.active_sessions); el('dash-runs', s.runs_today); el('dash-tools', s.tool_calls_today); el('dash-errors', s.errors_today); // Sub-line: framework breakdown for active sessions const fws = Object.keys(s.by_framework || {}); if (fws.length > 0) { const sub = document.getElementById('dash-active-sub'); if (sub) sub.textContent = fws.map(f => `${f} ${(s.by_framework[f].runs || 0)}`).join(' · '); } const errEl = document.getElementById('dash-errors'); if (errEl) { errEl.classList.toggle('has-errors', s.errors_today > 0); } // Metrics strip el('dash-tokens-today', formatTokenCount(s.tokens_today || 0)); el('dash-cost-today', s.cost_today ? formatCost(s.cost_today) : '$0.0000'); el('dash-avg-duration', s.avg_duration_ms ? formatDuration(s.avg_duration_ms) : '-'); const errorRateEl = document.getElementById('dash-error-rate'); if (errorRateEl) { const totalOps = (s.runs_today || 0) + (s.tool_calls_today || 0); const rate = totalOps > 0 ? ((s.errors_today || 0) / totalOps * 100) : 0; errorRateEl.textContent = rate.toFixed(1) + '%'; errorRateEl.classList.toggle('alert', rate > 5); } } async function loadTimeseries() { try { // Destroy chart so it's recreated with new window scale if (dashboardChart) { dashboardChart.destroy(); dashboardChart = null; } dashboardState.chartCursorIndex = null; const cachedWin = tryParseJSON(localStorage.getItem('agentmon:dash:ts:' + dashboardState.window)); if (cachedWin) { dashboardState.timeseries = cachedWin; renderTimeseriesChart(); renderRightPanel(); } const data = await api('/v1/stats/timeseries?window=' + dashboardState.window); if (!isCurrentPath('/')) return; dashboardState.timeseries = data; localStorage.setItem('agentmon:dash:ts:' + dashboardState.window, JSON.stringify(data)); renderTimeseriesChart(); renderRightPanel(); } catch (e) { console.error('Failed to load timeseries:', e); } } function getDashboardBucketIntervalMS() { const bucket = dashboardState && dashboardState.timeseries ? dashboardState.timeseries.bucket : ''; switch (bucket) { case '1m': return 60 * 1000; case '5m': return 5 * 60 * 1000; case '15m': return 15 * 60 * 1000; case '1h': return 60 * 60 * 1000; default: return 60 * 1000; } } function formatBucketLabel(ts) { const start = new Date(ts); if (Number.isNaN(start.getTime())) return '-'; const end = new Date(start.getTime() + getDashboardBucketIntervalMS()); const sameDay = start.toLocaleDateString() === end.toLocaleDateString(); const startLabel = start.toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', }); const endLabel = end.toLocaleString([], sameDay ? { hour: 'numeric', minute: '2-digit' } : { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); return startLabel + ' to ' + endLabel; } function getDashboardChartStats() { const ts = dashboardState.timeseries; if (!ts || !ts.series || ts.series.length === 0) return null; const totals = ts.series.map(b => (b.runs || 0) + (b.tools || 0) + (b.errors || 0)); const sum = values => values.reduce((acc, value) => acc + value, 0); let peakIndex = 0; for (let i = 1; i < totals.length; i++) { if (totals[i] > totals[peakIndex]) peakIndex = i; } return { totalRuns: sum(ts.series.map(b => b.runs || 0)), totalTools: sum(ts.series.map(b => b.tools || 0)), totalErrors: sum(ts.series.map(b => b.errors || 0)), totalEvents: sum(totals), peakIndex, peakTotal: totals[peakIndex] || 0, bucketCount: ts.series.length, }; } function buildChartData() { const ts = dashboardState.timeseries; if (!ts || !ts.series || ts.series.length === 0) return null; const timestamps = ts.series.map(b => Math.floor(new Date(b.ts).getTime() / 1000)); const runs = ts.series.map(b => b.runs || 0); const tools = ts.series.map(b => b.tools || 0); const errors = ts.series.map(b => b.errors || 0); const totals = ts.series.map((b, i) => runs[i] + tools[i] + errors[i]); if (dashboardState.chartMode === 'lines') { return [timestamps, totals, runs, tools, errors]; } const stackedTools = tools.map((value, i) => value + errors[i]); return [timestamps, totals, stackedTools, errors]; } function renderDashboardChartInsights() { const container = document.getElementById('dash-chart-insights'); if (!container) return; const stats = getDashboardChartStats(); if (!stats) { container.innerHTML = ''; return; } const peakBucket = dashboardState.timeseries.series[stats.peakIndex]; container.innerHTML = `
window total${escapeHTML(formatCount(stats.totalEvents))}
peak bucket${escapeHTML(formatCount(stats.peakTotal))}${escapeHTML(formatBucketLabel(peakBucket.ts))}
mix${escapeHTML(formatCount(stats.totalRuns))}r / ${escapeHTML(formatCount(stats.totalTools))}t / ${escapeHTML(formatCount(stats.totalErrors))}e
bucket${escapeHTML(dashboardState.timeseries.bucket || '-')}${escapeHTML(String(stats.bucketCount))} points
`; } function renderDashboardChartHover(idx) { const container = document.getElementById('dash-chart-hover'); if (!container) return; const ts = dashboardState.timeseries; if (!ts || !ts.series || ts.series.length === 0) { container.innerHTML = ''; return; } const safeIdx = Number.isInteger(idx) && idx >= 0 && idx < ts.series.length ? idx : ts.series.length - 1; const bucket = ts.series[safeIdx]; const prev = safeIdx > 0 ? ts.series[safeIdx - 1] : null; const total = (bucket.runs || 0) + (bucket.tools || 0) + (bucket.errors || 0); const prevTotal = prev ? (prev.runs || 0) + (prev.tools || 0) + (prev.errors || 0) : 0; const delta = total - prevTotal; const deltaLabel = (delta > 0 ? '+' : '') + delta; const bucketLabel = safeIdx === ts.series.length - 1 ? 'Latest bucket' : 'Selected bucket'; container.innerHTML = `
${escapeHTML(bucketLabel)}
${escapeHTML(formatBucketLabel(bucket.ts))}
Total ${escapeHTML(formatCount(total))}
Runs${escapeHTML(formatCount(bucket.runs || 0))}
Tools${escapeHTML(formatCount(bucket.tools || 0))}
Errors${escapeHTML(formatCount(bucket.errors || 0))}
Delta${escapeHTML(deltaLabel)}
`; } function renderTimeseriesChart() { const container = document.getElementById('dash-chart'); if (!container || !dashboardState.timeseries) return; const data = buildChartData(); renderDashboardChartInsights(); renderDashboardChartHover(dashboardState.chartCursorIndex); if (!data) { container.innerHTML = '

No data for this window

'; return; } // If chart already exists, just update the data if (dashboardChart) { dashboardChart.setData(data); return; } container.innerHTML = ''; const width = container.clientWidth || 600; const height = 200; const commonSeries = [ {}, { label: 'Total', stroke: '#f8fafc', width: 1.5, dash: [6, 4], points: { show: false }, }, ]; const lineSeries = [ ...commonSeries, { label: 'Runs', stroke: '#34d399', width: 1.75, fill: 'rgba(52, 211, 153, 0.08)', }, { label: 'Tools', stroke: '#22d3ee', width: 1.75, fill: 'rgba(34, 211, 238, 0.08)', }, { label: 'Errors', stroke: '#f87171', width: 1.75, fill: 'rgba(248, 113, 113, 0.08)', }, ]; const stackedSeries = [ ...commonSeries, { label: 'Tools+Errors', stroke: 'rgba(34, 211, 238, 0.85)', width: 1.25, points: { show: false }, }, { label: 'Errors', stroke: '#f87171', width: 1.25, points: { show: false }, fill: 'rgba(248, 113, 113, 0.18)', }, ]; const opts = { width, height, cursor: { show: true }, hooks: { setCursor: [ u => { dashboardState.chartCursorIndex = Number.isInteger(u.cursor.idx) ? u.cursor.idx : null; renderDashboardChartHover(dashboardState.chartCursorIndex); }, ], }, scales: { x: { time: true }, y: { auto: true, min: 0 }, }, axes: [ { stroke: '#4e6070', grid: { stroke: 'rgba(28, 38, 55, 0.6)', width: 1 }, ticks: { stroke: 'rgba(28, 38, 55, 0.6)', width: 1 }, font: '11px Fira Code', }, { stroke: '#4e6070', grid: { stroke: 'rgba(28, 38, 55, 0.6)', width: 1 }, ticks: { stroke: 'rgba(28, 38, 55, 0.6)', width: 1 }, font: '11px Fira Code', size: 50, }, ], series: dashboardState.chartMode === 'lines' ? lineSeries : stackedSeries, bands: dashboardState.chartMode === 'lines' ? [] : [ { series: [1, 2], fill: 'rgba(52, 211, 153, 0.18)' }, { series: [2, 3], fill: 'rgba(34, 211, 238, 0.18)' }, ], }; dashboardChart = new uPlot(opts, data, container); if (dashboardResizeObserver) { dashboardResizeObserver.disconnect(); } dashboardResizeObserver = new ResizeObserver(entries => { for (const entry of entries) { if (dashboardChart) { dashboardChart.setSize({ width: entry.contentRect.width, height: 200 }); } } }); dashboardResizeObserver.observe(container); } function appendToCurrentBucket(evt) { const ts = dashboardState.timeseries; if (!ts || !ts.series || ts.series.length === 0) return; const now = Math.floor(Date.now() / 60000) * 60000; const last = ts.series[ts.series.length - 1]; const lastTs = new Date(last.ts).getTime(); let bucket; if (Math.abs(now - lastTs) < 60000) { bucket = last; } else { bucket = { ts: new Date(now).toISOString(), runs: 0, tools: 0, errors: 0, tokens: 0, input_tokens: 0, output_tokens: 0, cost: 0, avg_duration_ms: 0 }; ts.series.push(bucket); } const eventType = getEnvelopeType(evt); if (eventType === 'run.start') bucket.runs++; if (eventType === 'error') bucket.errors++; if (eventType === 'span.end') { const attrs = getEnvelopeAttributes(evt); if (attrs.span_kind === 'tool') bucket.tools++; } if (eventType === 'run.end') { const payload = getEnvelopePayload(evt); const usage = payload.usage || {}; bucket.tokens = (bucket.tokens || 0) + (usage.total_tokens || 0); bucket.input_tokens = (bucket.input_tokens || 0) + (usage.input_tokens || 0); bucket.output_tokens = (bucket.output_tokens || 0) + (usage.output_tokens || 0); bucket.cost = (bucket.cost || 0) + (usage.total_cost || 0); if (payload.duration_ms) { const runCount = bucket.runs || 1; const prev = bucket.avg_duration_ms || 0; bucket.avg_duration_ms = prev + (payload.duration_ms - prev) / runCount; } } dashboardState.chartCursorIndex = ts.series.length - 1; renderTimeseriesChart(); } function renderRightPanel() { const mode = dashboardState && dashboardState.rightPanelMode; if (mode === 'tokens') { renderTokenPanel(); } else if (mode === 'latency') { renderLatencyPanel(); } else { renderFrameworkBars(); } } function renderTokenPanel() { const container = document.getElementById('dash-right-panel'); if (!container) return; const s = dashboardState.summary; const ts = dashboardState.timeseries; const totalTokens = s ? (s.tokens_today || 0) : 0; const inputTokens = ts && ts.series ? ts.series.reduce((acc, b) => acc + (b.input_tokens || 0), 0) : 0; const outputTokens = ts && ts.series ? ts.series.reduce((acc, b) => acc + (b.output_tokens || 0), 0) : 0; const totalCost = s ? (s.cost_today || 0) : 0; const maxIO = Math.max(inputTokens, outputTokens, 1); container.innerHTML = `
Total tokens today
${escapeHTML(formatTokenCount(totalTokens))}
Input
${escapeHTML(formatTokenCount(inputTokens))}
Output
${escapeHTML(formatTokenCount(outputTokens))}
Est. cost today ${escapeHTML(totalCost ? formatCost(totalCost) : '$0.0000')}
`; } function renderLatencyPanel() { const container = document.getElementById('dash-right-panel'); if (!container) return; const ts = dashboardState.timeseries; if (!ts || !ts.series || ts.series.length === 0) { container.innerHTML = '

No latency data

'; return; } const durSeries = ts.series.map(b => b.avg_duration_ms || 0).filter(v => v > 0); if (durSeries.length === 0) { container.innerHTML = '

No run latency recorded yet

'; return; } const avg = durSeries.reduce((a, b) => a + b, 0) / durSeries.length; const min = Math.min(...durSeries); const max = Math.max(...durSeries); const maxBar = max || 1; container.innerHTML = `
Min ${escapeHTML(formatDuration(min))}
Avg ${escapeHTML(formatDuration(avg))}
Max ${escapeHTML(formatDuration(max))}
${durSeries.map((v, i) => { const pct = (v / maxBar * 100).toFixed(1); const label = ts.series.filter(b => b.avg_duration_ms > 0)[i]; const title = label ? formatBucketLabel(label.ts) + ': ' + formatDuration(v) : formatDuration(v); return `
`; }).join('')}
Avg run duration per bucket (${escapeHTML(ts.bucket || '-')})
`; } function renderFrameworkBars() { const container = document.getElementById('dash-right-panel'); if (!container || !dashboardState.summary) return; const byFw = dashboardState.summary.by_framework || {}; const entries = Object.entries(byFw).sort((a, b) => { const totalA = a[1].runs + a[1].tools + a[1].errors; const totalB = b[1].runs + b[1].tools + b[1].errors; return totalB - totalA; }); if (entries.length === 0) { container.innerHTML = '

No framework data

'; return; } const maxTotal = Math.max(...entries.map(([, s]) => s.runs + s.tools + s.errors)); container.innerHTML = '
' + entries.map(([name, stats]) => { const total = stats.runs + stats.tools + stats.errors; const pct = maxTotal > 0 ? (total / maxTotal * 100) : 0; const cssClass = name.toLowerCase().replace(/[^a-z0-9-]/g, '-'); return `
${escapeHTML(name)} ${total} events
`; }).join('') + '
'; } function renderDashFeedItem(evt) { const eventType = getEnvelopeType(evt); const correlation = getEnvelopeCorrelation(evt); const vmName = getVMName(evt); const vmClass = getVMClassName(vmName); const source = getEnvelopeSource(evt); const framework = source.framework || ''; const tag = framework ? `${escapeHTML(framework)}` : ''; const sessionID = correlation.session_id || ''; const clickableClass = sessionID ? ' timeline-event-link' : ''; const attrs = sessionID ? ` role="link" tabindex="0" data-session-id="${escapeHTML(sessionID)}"` : ''; return `
${getEventIcon(eventType)} ${tag} ${escapeHTML(getEventLabel(eventType))} ${escapeHTML(new Date(getEnvelopeTS(evt)).toLocaleTimeString())}
${getEventBody(evt)}
`; } function renderDashFeed() { const feed = document.getElementById('dash-feed'); if (!feed) return; const recent = dashboardState.recentEvents.slice(-DASH_RECENT_EVENTS_LIMIT).reverse(); if (recent.length === 0) { feed.innerHTML = '

Waiting for events...

'; return; } feed.innerHTML = recent.map(renderDashFeedItem).join(''); feed.querySelectorAll('.timeline-event-link').forEach(item => { const sessionID = item.dataset.sessionId || ''; if (!sessionID) return; item.addEventListener('click', () => navigate('/sessions/' + sessionID)); item.addEventListener('keydown', event => { if (event.key !== 'Enter' && event.key !== ' ') { return; } event.preventDefault(); navigate('/sessions/' + sessionID); }); }); } function renderDashTopTools() { const list = document.getElementById('dash-top-tools'); if (!list) return; const topTools = Object.entries(dashboardState.toolCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); if (topTools.length === 0) { list.innerHTML = '
  • No tool data yet
  • '; return; } const maxCount = topTools[0]?.[1] || 1; list.innerHTML = topTools.map(([name, count]) => { const pct = (count / maxCount * 100).toFixed(1); return `
  • ${escapeHTML(name)} ${count}
  • `; }).join(''); } function renderDashTopModels() { const list = document.getElementById('dash-top-models'); if (!list) return; const topModels = Object.entries(dashboardState.modelCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); if (topModels.length === 0) { list.innerHTML = '
  • No model data yet
  • '; return; } const maxCount = topModels[0]?.[1] || 1; list.innerHTML = topModels.map(([name, count]) => { const pct = (count / maxCount * 100).toFixed(1); return `
  • ${escapeHTML(name)} ${count}
  • `; }).join(''); } route(); })();