// ── utils.js — pure helpers, no imports ────────────────── export function escapeHTML(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } export function tryParseJSON(s) { try { return s ? JSON.parse(s) : null; } catch { return null; } } export function animateCounter(elementId, newValue) { const elem = document.getElementById(elementId); if (!elem) return; const oldText = elem.textContent; const newText = String(newValue); if (oldText === newText) return; elem.textContent = newText; elem.classList.remove('bumped'); void elem.offsetWidth; // force reflow elem.classList.add('bumped'); } export 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'; } export 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'; } export 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]; } export function formatCount(value) { if (value === undefined || value === null || value === '') return '-'; return String(value); } export 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); } export 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); } export 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'; } export function statusIcon(status) { if (status === 'success') return 'success'; if (status === 'error') return 'error'; return 'unknown'; } export function skeletonRows(rows, cols) { return Array(rows).fill(0).map(() => '' + Array(cols).fill('
').join('') + '' ).join(''); } export function dashboardSkeleton() { return `
${Array(4).fill('
').join('')}
`; } export function sessionsSkeleton() { return Array(8).fill(0).map((_, i) => { const widths = [['55%','25%'], ['65%','20%'], ['45%','30%'], ['70%','15%'], ['50%','22%'], ['60%','18%'], ['42%','28%'], ['68%','12%']]; const [w1, w2] = widths[i % widths.length]; return `
`; }).join(''); } export function agentsSkeleton() { return `
${Array(4).fill('
').join('')}
`; } export function infrastructureSkeleton() { return `
${Array(6).fill('
').join('')}
`; } // ── Envelope helpers ───────────────────────────────────── export function extractEnvelope(record) { if (record && typeof record === 'object' && record.payload && record.payload.event && record.payload.schema) { return record.payload; } return record || {}; } export function getEnvelopeEvent(record) { const envelope = extractEnvelope(record); return envelope.event || envelope.Event || {}; } export function getEnvelopeType(record) { return record?.type || getEnvelopeEvent(record).type || ''; } export function getEnvelopeTS(record) { return record?.ts || getEnvelopeEvent(record).ts || ''; } export function getEnvelopeSource(record) { return getEnvelopeEvent(record).source || {}; } export function getEnvelopePayload(record) { const envelope = extractEnvelope(record); return envelope.payload || envelope.Payload || {}; } export function getEnvelopeAttributes(record) { const envelope = extractEnvelope(record); return envelope.attributes || envelope.Attributes || {}; } export function getEnvelopeCorrelation(record) { const envelope = extractEnvelope(record); return envelope.correlation || envelope.Correlation || {}; } export function getRecordID(record) { return record?.event_id || getEnvelopeEvent(record).id || ''; } export function isCurrentPath(prefix) { return window.location.pathname.startsWith(prefix); } // ── Agent identity helpers ─────────────────────────────── export function normalizeAgentKey(value) { return String(value || '') .trim() .toLowerCase() .replace(/[^a-z0-9._-]+/g, '-'); } export 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 }; } // ── VM/event display helpers ───────────────────────────── export function getVMName(evt) { return getAgentIdentity(evt).name || 'unknown'; } export function getVMClassName(vmName) { const normalized = String(vmName || 'unknown').toLowerCase(); return ['zap', 'orb', 'sun'].includes(normalized) ? normalized : 'unknown'; } export 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 '
·
'; } } export 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; } export 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 ''; } export 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); } export function isAgentTimelineEvent(evt) { const eventType = getEnvelopeType(evt); return [ 'session.start', 'session.end', 'run.start', 'run.end', 'span.start', 'span.end', 'error', ].includes(eventType); } export function isDashboardFeedEvent(evt) { const eventType = getEnvelopeType(evt); return isAgentTimelineEvent(evt) || eventType === 'metric.snapshot'; } // ── Run usage extraction ───────────────────────────────── export function extractRunUsage(spans) { let totalTokens = 0, inputTokens = 0, outputTokens = 0, totalCost = 0; let found = false; (spans || []).forEach(sp => { const inner = (sp.payload || {}).payload || {}; const checkUsage = (u) => { if (!u) return; if (u.total_tokens != null) { totalTokens = Math.max(totalTokens, Number(u.total_tokens) || 0); found = true; } if (u.input_tokens != null) inputTokens = Math.max(inputTokens, Number(u.input_tokens) || 0); if (u.output_tokens != null) outputTokens = Math.max(outputTokens, Number(u.output_tokens) || 0); if (u.total_cost != null) totalCost = Math.max(totalCost, Number(u.total_cost) || 0); }; checkUsage(inner.usage); if (inner.metrics) checkUsage(inner.metrics.usage); }); return found ? { totalTokens, inputTokens, outputTokens, totalCost } : null; } // ── Copy button ────────────────────────────────────────── export function renderCopyButton(text) { // data-copy is decoded in a single HTML-attribute context by the parser, // so escapeHTML is sufficient. A delegated click handler in app.js picks // these up and calls copyToClipboard — no inline onclick needed. return ``; }