// K8s Agent Dashboard - Frontend JavaScript const API_BASE = '/api'; // State let currentView = 'status'; // Live feed state let pendingLiveEvents = []; let liveEvents = []; let liveEventSource = null; // Initialize document.addEventListener('DOMContentLoaded', () => { setupNavigation(); loadAllData(); // Refresh data every 30 seconds setInterval(loadAllData, 30000); // Initialize live feed initLiveFeed(); // Batch render live events every 1s setInterval(() => { if (pendingLiveEvents.length > 0) { liveEvents = [...pendingLiveEvents, ...liveEvents]; if (liveEvents.length > 500) { liveEvents = liveEvents.slice(0, 500); } pendingLiveEvents = []; if (currentView === 'live') { renderLiveEvents(); } } }, 1000); }); // Navigation function setupNavigation() { document.querySelectorAll('.nav-btn').forEach(btn => { btn.addEventListener('click', () => { const view = btn.dataset.view; switchView(view); }); }); } function switchView(view) { currentView = view; // Update nav buttons document.querySelectorAll('.nav-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.view === view); }); // Update views document.querySelectorAll('.view').forEach(v => { v.classList.toggle('active', v.id === `${view}-view`); }); } // Data Loading async function loadAllData() { try { await Promise.all([ // Existing k8s dashboard data loadClusterStatus(), loadPendingActions(), loadHistory(), loadWorkflows(), // Claude dashboard data loadClaudeStats(), loadClaudeInventory(), loadClaudeDebugFiles() ]); updateLastUpdate(); } catch (error) { console.error('Error loading data:', error); } } async function initLiveFeed() { try { // Load initial backlog const response = await fetch(`${API_BASE}/claude/live/backlog?limit=200`); const data = await response.json(); liveEvents = data.events || []; renderLiveEvents(); // Setup SSE liveEventSource = new EventSource(`${API_BASE}/claude/stream`); liveEventSource.onopen = () => { updateLiveConnStatus('connected'); }; liveEventSource.onerror = () => { updateLiveConnStatus('error'); }; liveEventSource.onmessage = (e) => { try { const ev = JSON.parse(e.data); pendingLiveEvents.push(ev); } catch (err) { console.error('Error parsing SSE event:', err); } }; } catch (error) { console.error('Error initializing live feed:', error); updateLiveConnStatus('error'); } } function updateLiveConnStatus(status) { const el = document.getElementById('claude-live-conn'); if (!el) return; el.className = `conn-badge conn-badge-${status}`; el.textContent = status === 'connected' ? 'Connected' : status === 'error' ? 'Disconnected' : 'Connecting...'; } function renderLiveEvents() { const tbody = document.querySelector('#claude-live-table tbody'); if (!tbody) return; if (liveEvents.length === 0) { tbody.innerHTML = 'Waiting for events...'; return; } tbody.innerHTML = liveEvents.map(ev => { const summary = ev.data?.summary || {}; const summaryText = Object.values(summary).filter(Boolean).join(' ') || '-'; const display = ev.data?.json?.display || ''; return ` ${formatDateTime(ev.ts)} ${ev.type} ${summaryText} `; }).join(''); } function toggleRawJson(btn) { const pre = btn.nextElementSibling; if (pre.style.display === 'none') { pre.style.display = 'block'; btn.textContent = 'Hide JSON'; } else { pre.style.display = 'none'; btn.textContent = 'Show JSON'; } } // Claude dashboard data loading async function loadClaudeStats() { try { const response = await fetch(`${API_BASE}/claude/stats`); const data = await response.json(); renderClaudeOverview(data); renderClaudeUsage(data); } catch (error) { // Keep k8s dashboard working even if claude endpoints are unavailable console.error('Error loading Claude stats:', error); renderClaudeOverview(null); renderClaudeUsage(null); } } async function loadClaudeInventory() { try { const response = await fetch(`${API_BASE}/claude/inventory`); const data = await response.json(); renderClaudeInventory(data); } catch (error) { console.error('Error loading Claude inventory:', error); renderClaudeInventory(null); } } async function loadClaudeDebugFiles() { try { const response = await fetch(`${API_BASE}/claude/debug/files`); const data = await response.json(); renderClaudeDebugFiles(data); } catch (error) { console.error('Error loading Claude debug files:', error); renderClaudeDebugFiles(null); } } async function loadClusterStatus() { try { const response = await fetch(`${API_BASE}/status`); const data = await response.json(); renderClusterStatus(data); } catch (error) { console.error('Error loading status:', error); } } async function loadPendingActions() { try { const response = await fetch(`${API_BASE}/pending`); const data = await response.json(); renderPendingActions(data.actions || []); document.getElementById('pending-count').textContent = data.count || 0; } catch (error) { console.error('Error loading pending:', error); } } async function loadHistory() { try { const response = await fetch(`${API_BASE}/history?limit=20`); const data = await response.json(); renderHistory(data.history || []); } catch (error) { console.error('Error loading history:', error); } } async function loadWorkflows() { try { const response = await fetch(`${API_BASE}/workflows`); const data = await response.json(); renderWorkflows(data.workflows || []); } catch (error) { console.error('Error loading workflows:', error); } } // Rendering function renderClusterStatus(status) { // Update health indicator const healthEl = document.getElementById('cluster-health'); const indicator = healthEl.querySelector('.health-indicator'); const text = healthEl.querySelector('.health-text'); const health = (status.health || 'Unknown').toLowerCase(); indicator.className = `health-indicator ${health}`; text.textContent = status.health || 'Unknown'; // Render nodes const nodesBody = document.querySelector('#nodes-table tbody'); if (status.nodes && status.nodes.length > 0) { nodesBody.innerHTML = status.nodes.map(node => ` ${node.name} ${node.status}
${node.cpu_percent.toFixed(0)}%
${node.memory_percent.toFixed(0)}% ${node.conditions} `).join(''); } else { nodesBody.innerHTML = 'No nodes data available'; } // Render alerts const alertsList = document.getElementById('alerts-list'); if (status.alerts && status.alerts.length > 0) { alertsList.innerHTML = status.alerts.map(alert => `
[${alert.severity.toUpperCase()}] ${alert.name} ${alert.description}
`).join(''); } else { alertsList.innerHTML = '

No active alerts

'; } // Render apps const appsBody = document.querySelector('#apps-table tbody'); if (status.apps && status.apps.length > 0) { appsBody.innerHTML = status.apps.map(app => ` ${app.name} ${app.sync_status} ${app.health} ${app.revision.substring(0, 7)} `).join(''); } else { appsBody.innerHTML = 'No ArgoCD apps data available'; } } function renderPendingActions(actions) { const list = document.getElementById('pending-list'); if (actions.length === 0) { list.innerHTML = '

No pending actions

'; return; } list.innerHTML = actions.map(action => `
${action.agent}
${action.action}
${action.risk} risk
${action.description}
`).join(''); } function renderHistory(history) { const tbody = document.querySelector('#history-table tbody'); if (history.length === 0) { tbody.innerHTML = 'No history available'; return; } tbody.innerHTML = history.map(entry => ` ${formatTime(entry.timestamp)} ${entry.agent} ${entry.action} ${entry.result} `).join(''); } function renderWorkflows(workflows) { const list = document.getElementById('workflows-list'); if (workflows.length === 0) { list.innerHTML = '

No workflows defined

'; return; } list.innerHTML = workflows.map(wf => `

${wf.name}

${wf.description}

${wf.triggers.map(t => `${t}`).join('')}
`).join(''); } // Claude dashboard rendering function renderClaudeOverview(stats) { const el = document.getElementById('claude-overview'); if (!el) return; if (!stats) { el.innerHTML = '

Claude stats unavailable

'; return; } const lastComputedDate = stats.lastComputedDate || stats.lastComputed || stats.lastComputedAt || null; // Support both shapes: {totals:{...}} and flat {totalSessions,totalMessages,...} const totalSessions = (stats.totalSessions != null) ? stats.totalSessions : (stats.totals && stats.totals.sessions != null ? stats.totals.sessions : 0); const totalMessages = (stats.totalMessages != null) ? stats.totalMessages : (stats.totals && stats.totals.messages != null ? stats.totals.messages : 0); const totalToolCalls = (stats.totalToolCalls != null) ? stats.totalToolCalls : (stats.totals && (stats.totals.toolCalls != null ? stats.totals.toolCalls : (stats.totals.tools != null ? stats.totals.tools : 0))); el.innerHTML = `

Total Sessions

${totalSessions}

Total Messages

${totalMessages}

Total Tool Calls

${totalToolCalls}

Last Computed

${lastComputedDate ? formatDateTime(lastComputedDate) : 'Unknown'}
`; } function renderClaudeUsage(stats) { const tbody = document.querySelector('#claude-usage-table tbody'); if (!tbody) return; const daily = (stats && (stats.dailyActivity || stats.daily)) ? (stats.dailyActivity || stats.daily) : []; if (!daily || daily.length === 0) { tbody.innerHTML = 'No usage data available'; return; } tbody.innerHTML = daily.map(d => { const sessions = (d.sessionCount != null) ? d.sessionCount : (d.sessions != null ? d.sessions : 0); const messages = (d.messageCount != null) ? d.messageCount : (d.messages != null ? d.messages : 0); const toolCalls = (d.toolCallCount != null) ? d.toolCallCount : ((d.toolCalls != null) ? d.toolCalls : ((d.tools != null) ? d.tools : 0)); return ` ${d.date || d.day || ''} ${sessions} ${messages} ${toolCalls} `; }).join(''); } function renderClaudeInventory(inv) { const el = document.getElementById('claude-inventory'); if (!el) return; if (!inv) { el.innerHTML = '

Claude inventory unavailable

'; return; } const agents = inv.agents || []; const skills = inv.skills || []; const commands = inv.commands || []; el.innerHTML = `

Agents (${agents.length})

${renderSimpleList(agents.map(a => a.name || a.path || a))}

Skills (${skills.length})

${renderSimpleList(skills.map(s => s.name || s.path || s))}

Commands (${commands.length})

${renderSimpleList(commands.map(c => c.name || c.path || c))}
`; } function renderClaudeDebugFiles(debug) { const tbody = document.querySelector('#claude-debug-table tbody'); if (!tbody) return; const files = (debug && (debug.files || debug.keyFiles)) ? (debug.files || debug.keyFiles) : []; if (!files || files.length === 0) { tbody.innerHTML = 'No debug file info available'; return; } tbody.innerHTML = files.map(f => { const exists = (f.exists != null) ? f.exists : !f.missing; const status = (f.status || ((!exists) ? 'missing' : 'ok')).toLowerCase(); const badgeClass = status === 'ok' ? 'status-ok' : 'status-missing'; return ` ${escapeHtml(f.name || f.path || '')} ${status} ${(f.mtime || f.modTime) ? formatDateTime(f.mtime || f.modTime) : ''} ${f.error ? escapeHtml(f.error) : ''} `; }).join(''); } function renderSimpleList(items) { const safeItems = (items || []).filter(Boolean); if (safeItems.length === 0) return '

None

'; return ` `; } function formatDateTime(value) { const d = new Date(value); if (Number.isNaN(d.getTime())) return String(value); return d.toLocaleString(); } function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Actions async function approveAction(id) { try { const response = await fetch(`${API_BASE}/pending/${id}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); if (response.ok) { loadPendingActions(); loadHistory(); } else { alert('Failed to approve action'); } } catch (error) { console.error('Error approving action:', error); alert('Error approving action'); } } async function rejectAction(id) { const reason = prompt('Reason for rejection (optional):'); try { const response = await fetch(`${API_BASE}/pending/${id}/reject`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: reason || 'Rejected by user' }) }); if (response.ok) { loadPendingActions(); loadHistory(); } else { alert('Failed to reject action'); } } catch (error) { console.error('Error rejecting action:', error); alert('Error rejecting action'); } } async function runWorkflow(name) { try { const response = await fetch(`${API_BASE}/workflows/${name}/run`, { method: 'POST' }); const data = await response.json(); alert(data.message); } catch (error) { console.error('Error running workflow:', error); alert('Error running workflow'); } } // Helpers function getProgressClass(percent) { if (percent >= 80) return 'danger'; if (percent >= 60) return 'warning'; return ''; } function formatTime(timestamp) { const date = new Date(timestamp); return date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } function updateLastUpdate() { const now = new Date(); document.getElementById('last-update').textContent = now.toLocaleTimeString(); }