/**
* 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();