// 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}
Show JSON
${escapeHtml(JSON.stringify(ev.data?.json || ev.data?.rawLine || {}, null, 2))}
`;
}).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.description}
Approve
Reject
`).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('')}
Run
`).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 `
${safeItems.map(i => `${escapeHtml(String(i))} `).join('')}
`;
}
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();
}