// Dashboard Tab Component import { healthService } from '../services/health.service.js'; import { poseService } from '../services/pose.service.js'; export class DashboardTab { constructor(containerElement) { this.container = containerElement; this.statsElements = {}; this.healthSubscription = null; this.statsInterval = null; } // Initialize component async init() { this.cacheElements(); await this.loadInitialData(); this.startMonitoring(); } // Cache DOM elements cacheElements() { // System stats const statsContainer = this.container.querySelector('.system-stats'); if (statsContainer) { this.statsElements = { bodyRegions: statsContainer.querySelector('[data-stat="body-regions"] .stat-value'), samplingRate: statsContainer.querySelector('[data-stat="sampling-rate"] .stat-value'), accuracy: statsContainer.querySelector('[data-stat="accuracy"] .stat-value'), hardwareCost: statsContainer.querySelector('[data-stat="hardware-cost"] .stat-value') }; } // Status indicators this.statusElements = { apiStatus: this.container.querySelector('.api-status'), streamStatus: this.container.querySelector('.stream-status'), hardwareStatus: this.container.querySelector('.hardware-status') }; } // Load initial data async loadInitialData() { try { // Get API info const info = await healthService.getApiInfo(); this.updateApiInfo(info); // Get current stats const stats = await poseService.getStats(1); this.updateStats(stats); } catch (error) { // DensePose API may not be running (sensing-only mode) — fail silently console.log('Dashboard: DensePose API not available (sensing-only mode)'); } } // Start monitoring startMonitoring() { // Subscribe to health updates this.healthSubscription = healthService.subscribeToHealth(health => { this.updateHealthStatus(health); }); // Start periodic stats updates this.statsInterval = setInterval(() => { this.updateLiveStats(); }, 5000); // Start health monitoring healthService.startHealthMonitoring(30000); } // Update API info display updateApiInfo(info) { // Update version const versionElement = this.container.querySelector('.api-version'); if (versionElement && info.version) { versionElement.textContent = `v${info.version}`; } // Update environment const envElement = this.container.querySelector('.api-environment'); if (envElement && info.environment) { envElement.textContent = info.environment; envElement.className = `api-environment env-${info.environment}`; } // Update features status if (info.features) { this.updateFeatures(info.features); } } // Update features display updateFeatures(features) { const featuresContainer = this.container.querySelector('.features-status'); if (!featuresContainer) return; featuresContainer.innerHTML = ''; Object.entries(features).forEach(([feature, enabled]) => { const featureElement = document.createElement('div'); featureElement.className = `feature-item ${enabled ? 'enabled' : 'disabled'}`; featureElement.innerHTML = ` ${this.formatFeatureName(feature)} ${enabled ? '✓' : '✗'} `; featuresContainer.appendChild(featureElement); }); } // Update health status updateHealthStatus(health) { if (!health) return; // Update overall status const overallStatus = this.container.querySelector('.overall-health'); if (overallStatus) { overallStatus.className = `overall-health status-${health.status}`; overallStatus.textContent = health.status.toUpperCase(); } // Update component statuses if (health.components) { Object.entries(health.components).forEach(([component, status]) => { this.updateComponentStatus(component, status); }); } // Update metrics if (health.metrics) { this.updateSystemMetrics(health.metrics); } } // Update component status updateComponentStatus(component, status) { // Map backend component names to UI component names const componentMap = { 'pose': 'inference', 'stream': 'streaming', 'hardware': 'hardware' }; const uiComponent = componentMap[component] || component; const element = this.container.querySelector(`[data-component="${uiComponent}"]`); if (element) { element.className = `component-status status-${status.status}`; const statusText = element.querySelector('.status-text'); const statusMessage = element.querySelector('.status-message'); if (statusText) { statusText.textContent = status.status.toUpperCase(); } if (statusMessage && status.message) { statusMessage.textContent = status.message; } } // Also update API status based on overall health if (component === 'hardware') { const apiElement = this.container.querySelector(`[data-component="api"]`); if (apiElement) { apiElement.className = `component-status status-healthy`; const apiStatusText = apiElement.querySelector('.status-text'); const apiStatusMessage = apiElement.querySelector('.status-message'); if (apiStatusText) { apiStatusText.textContent = 'HEALTHY'; } if (apiStatusMessage) { apiStatusMessage.textContent = 'API server is running normally'; } } } } // Update system metrics updateSystemMetrics(metrics) { // Handle both flat and nested metric structures // Backend returns system_metrics.cpu.percent, mock returns metrics.cpu.percent const systemMetrics = metrics.system_metrics || metrics; const cpuPercent = systemMetrics.cpu?.percent || systemMetrics.cpu_percent; const memoryPercent = systemMetrics.memory?.percent || systemMetrics.memory_percent; const diskPercent = systemMetrics.disk?.percent || systemMetrics.disk_percent; // CPU usage const cpuElement = this.container.querySelector('.cpu-usage'); if (cpuElement && cpuPercent !== undefined) { cpuElement.textContent = `${cpuPercent.toFixed(1)}%`; this.updateProgressBar('cpu', cpuPercent); } // Memory usage const memoryElement = this.container.querySelector('.memory-usage'); if (memoryElement && memoryPercent !== undefined) { memoryElement.textContent = `${memoryPercent.toFixed(1)}%`; this.updateProgressBar('memory', memoryPercent); } // Disk usage const diskElement = this.container.querySelector('.disk-usage'); if (diskElement && diskPercent !== undefined) { diskElement.textContent = `${diskPercent.toFixed(1)}%`; this.updateProgressBar('disk', diskPercent); } } // Update progress bar updateProgressBar(type, percent) { const progressBar = this.container.querySelector(`.progress-bar[data-type="${type}"]`); if (progressBar) { const fill = progressBar.querySelector('.progress-fill'); if (fill) { fill.style.width = `${percent}%`; fill.className = `progress-fill ${this.getProgressClass(percent)}`; } } } // Get progress class based on percentage getProgressClass(percent) { if (percent >= 90) return 'critical'; if (percent >= 75) return 'warning'; return 'normal'; } // Update live statistics async updateLiveStats() { try { // Get current pose data const currentPose = await poseService.getCurrentPose(); this.updatePoseStats(currentPose); // Get zones summary const zonesSummary = await poseService.getZonesSummary(); this.updateZonesDisplay(zonesSummary); } catch (error) { console.error('Failed to update live stats:', error); } } // Update pose statistics updatePoseStats(poseData) { if (!poseData) return; // Update person count const personCount = this.container.querySelector('.person-count'); if (personCount) { const count = poseData.persons ? poseData.persons.length : (poseData.total_persons || 0); personCount.textContent = count; } // Update average confidence const avgConfidence = this.container.querySelector('.avg-confidence'); if (avgConfidence && poseData.persons && poseData.persons.length > 0) { const confidences = poseData.persons.map(p => p.confidence); const avg = confidences.length > 0 ? (confidences.reduce((a, b) => a + b, 0) / confidences.length * 100).toFixed(1) : 0; avgConfidence.textContent = `${avg}%`; } else if (avgConfidence) { avgConfidence.textContent = '0%'; } // Update total detections from stats if available const detectionCount = this.container.querySelector('.detection-count'); if (detectionCount && poseData.total_detections !== undefined) { detectionCount.textContent = this.formatNumber(poseData.total_detections); } } // Update zones display updateZonesDisplay(zonesSummary) { const zonesContainer = this.container.querySelector('.zones-summary'); if (!zonesContainer) return; zonesContainer.innerHTML = ''; // Handle different zone summary formats let zones = {}; if (zonesSummary && zonesSummary.zones) { zones = zonesSummary.zones; } else if (zonesSummary && typeof zonesSummary === 'object') { zones = zonesSummary; } // If no zones data, show default zones if (Object.keys(zones).length === 0) { ['zone_1', 'zone_2', 'zone_3', 'zone_4'].forEach(zoneId => { const zoneElement = document.createElement('div'); zoneElement.className = 'zone-item'; zoneElement.innerHTML = ` ${zoneId} undefined `; zonesContainer.appendChild(zoneElement); }); return; } Object.entries(zones).forEach(([zoneId, data]) => { const zoneElement = document.createElement('div'); zoneElement.className = 'zone-item'; const count = typeof data === 'object' ? (data.person_count || data.count || 0) : data; zoneElement.innerHTML = ` ${zoneId} ${count} `; zonesContainer.appendChild(zoneElement); }); } // Update statistics updateStats(stats) { if (!stats) return; // Update detection count const detectionCount = this.container.querySelector('.detection-count'); if (detectionCount && stats.total_detections !== undefined) { detectionCount.textContent = this.formatNumber(stats.total_detections); } // Update accuracy if available if (this.statsElements.accuracy && stats.average_confidence !== undefined) { this.statsElements.accuracy.textContent = `${(stats.average_confidence * 100).toFixed(1)}%`; } } // Format feature name formatFeatureName(name) { return name.replace(/_/g, ' ') .split(' ') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } // Format large numbers formatNumber(num) { if (num >= 1000000) { return `${(num / 1000000).toFixed(1)}M`; } if (num >= 1000) { return `${(num / 1000).toFixed(1)}K`; } return num.toString(); } // Show error message showError(message) { const errorContainer = this.container.querySelector('.error-container'); if (errorContainer) { errorContainer.textContent = message; errorContainer.style.display = 'block'; setTimeout(() => { errorContainer.style.display = 'none'; }, 5000); } } // Clean up dispose() { if (this.healthSubscription) { this.healthSubscription(); } if (this.statsInterval) { clearInterval(this.statsInterval); } healthService.stopHealthMonitoring(); } }