From bc3524e1630c0530eba080a2bda4caa1d76fe9a8 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Wed, 7 Jan 2026 22:43:13 -0500 Subject: [PATCH] Phase 2: Enhanced worker status display with metadata Changes: - Show worker system metrics in dashboard and worker list - Display CPU cores, memory usage, load average, uptime - Added formatBytes() to display memory in human-readable format - Added formatUptime() to show uptime as days/hours/minutes - Added getTimeAgo() to show relative last-seen time - Improved worker list with detailed metadata panel - Show active tasks vs max concurrent tasks - Terminal-themed styling for metadata display - Amber labels for metadata fields Benefits: - See worker health at a glance - Monitor resource usage (CPU, RAM, load) - Track worker activity (active tasks) - Better operational visibility Co-Authored-By: Claude Sonnet 4.5 --- public/index.html | 131 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 113 insertions(+), 18 deletions(-) diff --git a/public/index.html b/public/index.html index dca6106..27838a5 100644 --- a/public/index.html +++ b/public/index.html @@ -653,6 +653,43 @@ color: var(--terminal-green); font-family: var(--font-mono); } + + /* Worker Metadata Styles */ + .worker-stats { + display: flex; + gap: 15px; + margin-top: 8px; + font-size: 0.85em; + color: var(--terminal-green); + font-family: var(--font-mono); + } + + .worker-stats span { + padding: 2px 6px; + background: #001a00; + border: 1px solid #003300; + } + + .worker-metadata { + margin-top: 12px; + padding: 10px; + background: #001a00; + border: 1px solid #003300; + font-family: var(--font-mono); + font-size: 0.85em; + } + + .meta-row { + display: flex; + padding: 4px 0; + color: var(--terminal-green); + } + + .meta-label { + color: var(--terminal-amber); + min-width: 120px; + font-weight: bold; + } @@ -809,40 +846,69 @@ } // Dashboard view - const dashHtml = workers.length === 0 ? + const dashHtml = workers.length === 0 ? '
No workers connected
' : - workers.map(w => ` -
- ${w.status} - ${w.name} -
Last seen: ${new Date(w.last_heartbeat).toLocaleString()}
-
- `).join(''); + workers.map(w => { + const meta = w.metadata ? (typeof w.metadata === 'string' ? JSON.parse(w.metadata) : w.metadata) : null; + const lastSeen = getTimeAgo(new Date(w.last_heartbeat)); + return ` +
+ [${w.status === 'online' ? '●' : '○'}] + ${w.name} + ${meta ? `
+ CPU: ${meta.cpus || '?'} cores + RAM: ${formatBytes(meta.freeMem)}/${formatBytes(meta.totalMem)} + Tasks: ${meta.activeTasks || 0}/${meta.maxConcurrentTasks || 0} +
` : ''} +
Last seen: ${lastSeen}
+
+ `; + }).join(''); document.getElementById('dashWorkers').innerHTML = dashHtml; // Full worker list const fullHtml = workers.length === 0 ? '
No workers connected
' : workers.map(w => { - const meta = typeof w.metadata === 'string' ? JSON.parse(w.metadata) : w.metadata; + const meta = w.metadata ? (typeof w.metadata === 'string' ? JSON.parse(w.metadata) : w.metadata) : null; + const lastSeen = getTimeAgo(new Date(w.last_heartbeat)); + const memUsagePercent = meta && meta.totalMem ? ((meta.totalMem - meta.freeMem) / meta.totalMem * 100).toFixed(1) : 0; + const loadAvg = meta && meta.loadavg ? meta.loadavg.map(l => l.toFixed(2)).join(', ') : 'N/A'; + return `
-
- ${w.status} +
+ [${w.status === 'online' ? '●' : '○'}] ${w.name} -
ID: ${w.id}
-
Last heartbeat: ${new Date(w.last_heartbeat).toLocaleString()}
+
Last seen: ${lastSeen}
${meta ? ` -
-
CPUs: ${meta.cpus || 'N/A'} | RAM: ${meta.totalMem ? (meta.totalMem / 1024 / 1024 / 1024).toFixed(1) + 'GB' : 'N/A'}
-
Platform: ${meta.platform || 'N/A'} | Arch: ${meta.arch || 'N/A'}
-
Active Tasks: ${meta.activeTasks || 0}/${meta.maxConcurrentTasks || 0}
+ ` : ''}
${currentUser && currentUser.isAdmin ? ` - + ` : ''}
@@ -1051,6 +1117,35 @@ return div.innerHTML; } + function formatBytes(bytes) { + if (!bytes || bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i]; + } + + function formatUptime(seconds) { + if (!seconds) return 'N/A'; + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + if (days > 0) return `${days}d ${hours}h ${minutes}m`; + if (hours > 0) return `${hours}h ${minutes}m`; + return `${minutes}m`; + } + + function getTimeAgo(date) { + const seconds = Math.floor((new Date() - date) / 1000); + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; + } + async function respondToPrompt(executionId, response) { try { const res = await fetch(`/api/executions/${executionId}/respond`, {