1/** 2 * Overview Panel - System metrics and status display 3 * Shows active agents, success rate, costs, and recent activity 4 */ 5 6import { api } from '../../../common/api.js'; 7import { MetricWidget } from '../../../agents/components/index.js'; 8import PollingManager from '../../../agents/pollingManager.js'; 9import ActivityMonitor from '../../../agents/activityMonitor.js'; 10 11class OverviewPanel { 12 constructor(dashboard) { 13 this.dashboard = dashboard; 14 this.pollingManager = PollingManager.getInstance(); 15 this.activityMonitor = ActivityMonitor.getInstance(); 16 this.unsubscribers = []; 17 } 18 19 async render(container) { 20 container.innerHTML = ` 21 <div class="overview-panel"> 22 <div class="d-flex justify-content-between align-items-center mb-4"> 23 <h3>AI System Overview</h3> 24 <div> 25 <span class="badge bg-secondary me-2" id="polling-state">Loading...</span> 26 <button class="btn btn-sm btn-outline-primary" id="refreshMetrics"> 27 <i class="bi bi-arrow-clockwise"></i> Refresh 28 </button> 29 </div> 30 </div> 31 32 <div class="metrics-grid mb-4" id="metrics-grid"> 33 <div class="text-center text-muted">Loading metrics...</div> 34 </div> 35 36 <div class="row"> 37 <div class="col-md-6"> 38 <div class="card"> 39 <div class="card-header"> 40 <h5 class="mb-0">Recent Activity</h5> 41 </div> 42 <div class="card-body" id="recent-activity"> 43 <div class="text-center text-muted">Loading activity...</div> 44 </div> 45 </div> 46 </div> 47 48 <div class="col-md-6"> 49 <div class="card"> 50 <div class="card-header"> 51 <h5 class="mb-0">System Health</h5> 52 </div> 53 <div class="card-body" id="system-health"> 54 <div class="text-center text-muted">Loading health...</div> 55 </div> 56 </div> 57 </div> 58 </div> 59 </div> 60 `; 61 62 this.setupEventListeners(); 63 this.setupPolling(); 64 65 // Start activity monitor 66 this.activityMonitor.start(); 67 68 // Update polling state display 69 this.updatePollingStateDisplay(); 70 } 71 72 setupEventListeners() { 73 const refreshBtn = document.getElementById('refreshMetrics'); 74 if (refreshBtn) { 75 refreshBtn.addEventListener('click', () => { 76 this.pollingManager.refresh('/api/agents/metrics'); 77 this.pollingManager.refresh('/api/agents/tasks?limit=5'); 78 }); 79 } 80 } 81 82 setupPolling() { 83 // Subscribe to metrics endpoint 84 const metricsUnsub = this.pollingManager.subscribe( 85 '/api/agents/metrics', 86 (response) => this.handleMetricsUpdate(response) 87 ); 88 this.unsubscribers.push(metricsUnsub); 89 90 // Subscribe to tasks endpoint 91 const tasksUnsub = this.pollingManager.subscribe( 92 '/api/agents/tasks?limit=5', 93 (response) => this.handleTasksUpdate(response) 94 ); 95 this.unsubscribers.push(tasksUnsub); 96 97 // Listen for activity state changes 98 const activityUnsub = this.activityMonitor.onChange((newState) => { 99 console.log('Activity state changed:', newState); 100 this.pollingManager.setActivityState(newState); 101 this.updatePollingStateDisplay(); 102 }); 103 this.unsubscribers.push(activityUnsub); 104 } 105 106 handleMetricsUpdate(response) { 107 // Handle errors and rate limiting 108 if (response.error || response.rateLimited) { 109 if (response.rateLimited) { 110 this.showRateLimitWarning(response.backoffDelay); 111 } 112 return; 113 } 114 115 const metrics = response.data || this.getDefaultMetrics(); 116 this.renderMetrics(metrics); 117 this.renderHealth(metrics); 118 119 // Feed data to ActivityMonitor (prevents direct API calls) 120 this.activityMonitor.updateActivityData({ 121 activeAgents: metrics.activeAgents || 0 122 }); 123 } 124 125 handleTasksUpdate(response) { 126 // Handle errors and rate limiting 127 if (response.error || response.rateLimited) { 128 return; 129 } 130 131 const tasks = response.data?.tasks || []; 132 this.renderActivity(tasks); 133 134 // Feed data to ActivityMonitor (prevents direct API calls) 135 const recentTasks = tasks.filter(task => 136 task.status === 'running' || task.status === 'pending' 137 ).length; 138 this.activityMonitor.updateActivityData({ 139 recentTasks 140 }); 141 } 142 143 showRateLimitWarning(backoffDelay) { 144 const grid = document.getElementById('metrics-grid'); 145 if (grid && !grid.querySelector('.rate-limit-warning')) { 146 const warning = document.createElement('div'); 147 warning.className = 'alert alert-warning rate-limit-warning'; 148 warning.innerHTML = ` 149 <i class="bi bi-exclamation-triangle"></i> 150 Rate limited. Backing off for ${Math.round(backoffDelay / 1000)}s... 151 `; 152 grid.prepend(warning); 153 154 // Remove after backoff period 155 setTimeout(() => warning.remove(), backoffDelay); 156 } 157 } 158 159 updatePollingStateDisplay() { 160 const badge = document.getElementById('polling-state'); 161 if (!badge) return; 162 163 const state = this.activityMonitor.getState(); 164 const metrics = this.pollingManager.getMetrics(); 165 166 badge.className = 'badge me-2'; 167 badge.className += state === 'IDLE' ? ' bg-secondary' : 168 state === 'ACTIVE' ? ' bg-primary' : 169 state === 'PEAK' ? ' bg-warning' : ' bg-secondary'; 170 171 badge.textContent = `${state} (Cache: ${Math.round(metrics.cacheHitRate * 100)}%)`; 172 badge.title = `Polling: ${state}\nCache Hit Rate: ${Math.round(metrics.cacheHitRate * 100)}%\nActive Endpoints: ${metrics.activeEndpoints}`; 173 } 174 175 getDefaultMetrics() { 176 return { 177 activeAgents: 0, 178 totalAgents: 0, 179 completedTasks: 0, 180 failedTasks: 0, 181 successRate: 0, 182 dailyCost: 0, 183 monthlyCost: 0, 184 avgTaskDuration: 0 185 }; 186 } 187 188 renderMetrics(metrics) { 189 const grid = document.getElementById('metrics-grid'); 190 if (!grid) return; 191 192 grid.innerHTML = ''; 193 194 // Show "Coming Soon" message if no real data 195 const hasData = metrics.totalAgents > 0 || metrics.completedTasks > 0; 196 if (!hasData) { 197 grid.innerHTML = ` 198 <div class="alert alert-info border-0" style="grid-column: 1 / -1;"> 199 <div class="d-flex align-items-center"> 200 <i class="bi bi-info-circle me-2" style="font-size: 1.5rem;"></i> 201 <div> 202 <h6 class="mb-1">Parallel AI Monitoring Coming Soon</h6> 203 <small class="text-muted"> 204 Metrics will appear here when parallel AI tasks are running. 205 Send complex requests to see agents in action! 206 </small> 207 </div> 208 </div> 209 </div> 210 `; 211 return; 212 } 213 214 const widgets = [ 215 { 216 icon: 'bi-lightning-charge', 217 label: 'Active Agents', 218 value: metrics.activeAgents || 0, 219 color: 'primary', 220 subtitle: `${metrics.totalAgents || 0} total` 221 }, 222 { 223 icon: 'bi-check-circle', 224 label: 'Success Rate',
225 value: `${((metrics.successRate || 0) * 100).toFixed(1)}%`, 226 color: metrics.successRate >= 0.9 ? 'success' : metrics.successRate >= 0.7 ? 'warning' : 'danger', 227 subtitle: `${metrics.completedTasks || 0} completed` 228 }, 229 { 230 icon: 'bi-cash', 231 label: 'Today\'s Cost', 232 value: `$${(metrics.dailyCost || 0).toFixed(2)}`, 233 color: 'warning', 234 subtitle: `$${(metrics.monthlyCost || 0).toFixed(2)} this month` 235 }, 236 { 237 icon: 'bi-clock', 238 label: 'Avg Duration', 239 value: this.formatDuration(metrics.avgTaskDuration || 0), 240 color: 'info', 241 subtitle: `${metrics.completedTasks || 0} tasks` 242 } 243 ]; 244 245 widgets.forEach(config => { 246 const widget = new MetricWidget(config); 247 widget.render(grid); 248 }); 249 } 250 251 renderActivity(tasks) { 252 const container = document.getElementById('recent-activity'); 253 if (!container) return; 254 255 if (tasks.length === 0) { 256 container.innerHTML = ` 257 <div class="text-center py-4"> 258 <i class="bi bi-hourglass text-muted" style="font-size: 2rem;"></i> 259 <p class="text-muted mb-0 mt-2">No tasks yet</p> 260 <small class="text-muted">Activity will appear when AI tasks run</small> 261 </div> 262 `; 263 return; 264 } 265 266 container.innerHTML = ` 267 <div class="activity-list"> 268 ${tasks.map(task => ` 269 <div class="activity-item"> 270 <div class="d-flex justify-content-between align-items-start mb-1"> 271 <div> 272 <strong>${this.escapeHtml(task.mode || 'Unknown')}</strong> 273 <span class="badge bg-${this.getStatusColor(task.status)} ms-2"> 274 ${this.escapeHtml(task.status)} 275 </span> 276 </div> 277 <small class="text-muted">${this.formatTime(task.createdAt)}</small> 278 </div> 279 <div class="text-muted small"> 280 ${this.escapeHtml(task.description || 'No description')} 281 </div> 282 ${task.agents ? ` 283 <div class="mt-1"> 284 <small class="text-muted">${task.agents.length} agents</small> 285 </div> 286 ` : ''} 287 </div> 288 `).join('')} 289 </div> 290 `; 291 } 292 293 renderHealth(metrics) { 294 const container = document.getElementById('system-health'); 295 if (!container) return; 296 297 const health = this.calculateHealth(metrics); 298 299 container.innerHTML = ` 300 <div class="health-indicators"> 301 <div class="health-item"> 302 <div class="d-flex justify-content-between align-items-center mb-2"> 303 <span>Overall Health</span> 304 <span class="badge bg-${health.overall.color}">${health.overall.label}</span> 305 </div> 306 <div class="progress" style="height: 8px;"> 307 <div class="progress-bar bg-${health.overall.color}" 308 style="width: ${health.overall.score}%"></div> 309 </div> 310 </div> 311 312 <div class="health-item mt-3"> 313 <div class="d-flex justify-content-between mb-2"> 314 <span class="small">Success Rate</span> 315 <span class="small">${((metrics.successRate || 0) * 100).toFixed(0)}%</span> 316 </div> 317 <div class="progress" style="height: 6px;"> 318 <div class="progress-bar bg-${health.successRate.color}" 319 style="width: ${(metrics.successRate || 0) * 100}%"></div> 320 </div> 321 </div> 322 323 <div class="health-item mt-3"> 324 <div class="d-flex justify-content-between mb-2"> 325 <span class="small">Active Load</span> 326 <span class="small">${metrics.activeAgents || 0}/20</span> 327 </div> 328 <div class="progress" style="height: 6px;"> 329 <div class="progress-bar bg-${health.load.color}" 330 style="width: ${((metrics.activeAgents || 0) / 20) * 100}%"></div> 331 </div> 332 </div> 333 334 <div class="mt-3"> 335 <small class="text-muted"> 336 <i class="bi bi-info-circle"></i> 337 ${health.message} 338 </small> 339 </div> 340 </div> 341 `; 342 } 343 344 calculateHealth(metrics) { 345 const successRate = metrics.successRate || 0; 346 const activeLoad = (metrics.activeAgents || 0) / 20; 347 348 let overallScore = (successRate * 60) + ((1 - activeLoad) * 40); 349 overallScore = Math.min(100, Math.max(0, overallScore * 100)); 350 351 let overall = { score: overallScore }; 352 if (overallScore >= 80) { 353 overall = { ...overall, color: 'success', label: 'Excellent' }; 354 } else if (overallScore >= 60) { 355 overall = { ...overall, color: 'info', label: 'Good' }; 356 } else if (overallScore >= 40) { 357 overall = { ...overall, color: 'warning', label: 'Fair' }; 358 } else { 359 overall = { ...overall, color: 'danger', label: 'Poor' }; 360 } 361 362 const successRateHealth = { 363 color: successRate >= 0.9 ? 'success' : successRate >= 0.7 ? 'warning' : 'danger' 364 }; 365 366 const loadHealth = { 367 color: activeLoad <= 0.7 ? 'success' : activeLoad <= 0.9 ? 'warning' : 'danger' 368 }; 369 370 let message = 'System operating normally'; 371 if (successRate < 0.7) { 372 message = 'Success rate below target. Review failed tasks.'; 373 } else if (activeLoad > 0.9) { 374 message = 'High system load. Consider increasing capacity.'; 375 } 376 377 return { 378 overall, 379 successRate: successRateHealth, 380 load: loadHealth, 381 message 382 }; 383 } 384 385 renderError() { 386 const grid = document.getElementById('metrics-grid'); 387 if (grid) { 388 grid.innerHTML = ` 389 <div class="alert alert-info border-0" style="grid-column: 1 / -1;"> 390 <div class="d-flex align-items-center"> 391 <i class="bi bi-info-circle me-2" style="font-size: 1.5rem;"></i> 392 <div> 393 <h6 class="mb-1">Dashboard Ready</h6> 394 <small class="text-muted"> 395 Waiting for parallel AI tasks to run. The monitoring system will activate automatically 396 when you send complex requests that require multiple agents. 397 </small> 398 </div> 399 </div> 400 </div> 401 `; 402 } 403 } 404 405 formatDuration(seconds) { 406 if (seconds < 60) return `${seconds.toFixed(0)}s`; 407 const minutes = Math.floor(seconds / 60); 408 const secs = Math.floor(seconds % 60); 409 return `${minutes}m ${secs}s`; 410 } 411 412 formatTime(timestamp) { 413 if (!timestamp) return 'Unknown'; 414 const date = new Date(timestamp); 415 const now = new Date(); 416 const diff = (now - date) / 1000; // seconds 417 418 if (diff < 60) return 'Just now'; 419 if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; 420 if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; 421 return date.toLocaleDateString(); 422 } 423 424 getStatusColor(status) { 425 const colors = { 426 completed: 'success', 427 failed: 'danger', 428 running: 'primary', 429 pending: 'secondary', 430 cancelled: 'warning' 431 }; 432 return colors[status] || 'secondary'; 433 } 434 435 escapeHtml(text) { 436 const div = document.createElement('div'); 437 div.textContent = text; 438 return div.innerHTML; 439 } 440 441 destroy() { 442 // Unsubscribe from all polling
443 this.unsubscribers.forEach(unsub => unsub()); 444 this.unsubscribers = []; 445 } 446} 447 448export default OverviewPanel;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.