/** * AgentOS Dashboard — WebSocket-driven real-time updates. * * No framework, no build step. Pure vanilla JS. * Connects to the management port WebSocket at /ws. */ // State let ws = null; let reconnectTimeout = null; let messageCount = 0; const API_BASE = ''; // Same origin as management server // ========================================================================= // WebSocket Connection // ========================================================================= function connect() { const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${location.host}/ws`; ws = new WebSocket(wsUrl); ws.onopen = () => { setConnectionStatus(true); // Request full state on connect fetchInitialState(); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); handleEvent(data); } catch (e) { console.error('Failed to parse WebSocket message:', e); } }; ws.onclose = () => { setConnectionStatus(false); // Reconnect after delay reconnectTimeout = setTimeout(connect, 3000); }; ws.onerror = () => { ws.close(); }; } function setConnectionStatus(connected) { const el = document.getElementById('connection-status'); if (connected) { el.textContent = 'Connected'; el.className = 'status-indicator connected'; } else { el.textContent = 'Disconnected'; el.className = 'status-indicator disconnected'; } } // ========================================================================= // Event Handling // ========================================================================= function handleEvent(data) { const event = data.event; switch (event) { case 'connected': if (data.state) { updateFullState(data.state); } break; case 'agent_state': updateAgentState(data); break; case 'message': addMessage(data); messageCount++; updateMessageCount(); break; case 'thread_created': case 'thread_completed': fetchThreads(); break; default: break; } } // ========================================================================= // API Fetching // ========================================================================= async function fetchJSON(path) { try { const resp = await fetch(`${API_BASE}${path}`); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); return await resp.json(); } catch (e) { console.error(`Fetch ${path} failed:`, e); return null; } } async function fetchInitialState() { const [organism, agents, threads, usage] = await Promise.all([ fetchJSON('/api/v1/organism'), fetchJSON('/api/v1/agents'), fetchJSON('/api/v1/threads'), fetchJSON('/api/v1/usage'), ]); if (organism) { document.getElementById('organism-name').textContent = organism.name; updateUptime(organism.uptimeSeconds || 0); } if (agents) { renderAgents(agents.agents || []); const count = (agents.agents || []).length; const active = (agents.agents || []).filter(a => a.state === 'processing').length; document.getElementById('agent-count').textContent = count; document.getElementById('agent-detail').textContent = `${active} active`; } if (threads) { renderThreads(threads.threads || []); document.getElementById('thread-count').textContent = (threads.threads || []).length; } if (usage) { updateUsageCards(usage); } // Fetch audit refreshAudit(); } async function fetchThreads() { const threads = await fetchJSON('/api/v1/threads'); if (threads) { renderThreads(threads.threads || []); document.getElementById('thread-count').textContent = (threads.threads || []).length; } } // ========================================================================= // Rendering // ========================================================================= function renderAgents(agents) { const tbody = document.getElementById('agents-table'); if (!agents.length) { tbody.innerHTML = 'No agents registered'; return; } tbody.innerHTML = agents.map(a => ` ${escapeHtml(a.name)} ${a.isAgent ? 'Agent' : 'Tool'} ${a.state || 'idle'} ${(a.peers || []).map(p => `${escapeHtml(p)}`).join(', ') || '--'} ${a.messageCount || 0} `).join(''); } function renderThreads(threads) { const tbody = document.getElementById('threads-table'); if (!threads.length) { tbody.innerHTML = 'No active threads'; return; } tbody.innerHTML = threads.map(t => ` ${escapeHtml((t.threadId || t.id || '').substring(0, 8))}... ${t.status || 'active'} ${(t.participants || []).map(p => `${escapeHtml(p)}`).join(', ') || '--'} ${t.messageCount || 0} ${t.createdAt ? formatTime(t.createdAt) : '--'} `).join(''); } function addMessage(data) { const log = document.getElementById('message-log'); const empty = log.querySelector('.empty'); if (empty) empty.remove(); const entry = document.createElement('div'); entry.className = 'log-entry'; entry.innerHTML = ` ${formatTime(Date.now() / 1000)} ${escapeHtml(data.from || data.fromId || '?')} ${escapeHtml(data.payloadType || data.payload_type || JSON.stringify(data).substring(0, 200))} `; log.insertBefore(entry, log.firstChild); // Limit log entries while (log.children.length > 500) { log.removeChild(log.lastChild); } } function updateAgentState(data) { // Re-fetch agents on state change fetchJSON('/api/v1/agents').then(agents => { if (agents) { renderAgents(agents.agents || []); const count = (agents.agents || []).length; const active = (agents.agents || []).filter(a => a.state === 'processing').length; document.getElementById('agent-count').textContent = count; document.getElementById('agent-detail').textContent = `${active} active`; } }); } function updateUsageCards(usage) { if (usage.totals) { const tokens = usage.totals.totalTokens || 0; const cost = usage.totals.totalCost || usage.totals.estimatedCost || 0; document.getElementById('token-count').textContent = formatNumber(tokens); document.getElementById('token-cost').textContent = `$${cost.toFixed(4)}`; } } function updateMessageCount() { document.getElementById('message-count').textContent = formatNumber(messageCount); } function updateUptime(seconds) { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); document.getElementById('uptime').textContent = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; } // ========================================================================= // Audit Log // ========================================================================= async function refreshAudit() { const severity = document.getElementById('audit-severity').value; const type = document.getElementById('audit-type').value; let url = '/api/v1/audit/events?limit=200'; if (severity) url += `&severity=${severity}`; if (type) url += `&event_type=${type}`; const data = await fetchJSON(url); if (!data) return; const log = document.getElementById('audit-log'); if (!data.events || !data.events.length) { log.innerHTML = '
No audit events matching filters
'; return; } log.innerHTML = data.events.map(e => `
${formatTime(e.timestamp)} [${e.severity.toUpperCase()}] ${escapeHtml(e.listener_name)} ${escapeHtml(e.event_type)}: ${escapeHtml(JSON.stringify(e.details).substring(0, 300))}
`).join(''); } // Make refreshAudit available globally for the onclick handler window.refreshAudit = refreshAudit; // ========================================================================= // Tab Navigation // ========================================================================= document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); tab.classList.add('active'); const target = document.getElementById(`tab-${tab.dataset.tab}`); if (target) target.classList.add('active'); // Refresh data for active tab if (tab.dataset.tab === 'audit') refreshAudit(); if (tab.dataset.tab === 'threads') fetchThreads(); }); }); // ========================================================================= // Utilities // ========================================================================= function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = String(str); return div.innerHTML; } function formatTime(timestamp) { const d = new Date(timestamp * 1000); return d.toLocaleTimeString(); } function formatNumber(n) { 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); } // ========================================================================= // Periodic Refresh // ========================================================================= setInterval(() => { // Refresh uptime fetchJSON('/health').then(data => { if (data && data.uptime_seconds !== undefined) { updateUptime(data.uptime_seconds); } }); }, 10000); setInterval(() => { // Refresh usage fetchJSON('/api/v1/usage').then(data => { if (data) updateUsageCards(data); }); }, 30000); // ========================================================================= // Boot // ========================================================================= connect();